[Tile-serving] [osm2pgsql-dev/osm2pgsql] Some postal_code boundaries of OSM fail to import using as_multipolygon() (Issue #2414)

Marc Erdmann notifications at github.com
Tue Sep 9 15:32:33 UTC 2025


MarcErdmann created an issue (osm2pgsql-dev/osm2pgsql#2414)

## What version of osm2pgsql are you using?

osm2pgsql version 2.1.1
Build: Release
Compiled using the following library versions:
Libosmium 2.22.0
Proj 9.6.2
Lua 5.1.4 (LuaJIT 2.1.1744318430)

## What operating system and PostgreSQL/PostGIS version are you using?

Database version: 17.5 (Ubuntu 17.5-1.pgdg22.04+1) running in Docker
PostGIS version: 3.5

## Tell us something about your system

32 GB RAM, Apple M4

## What did you do exactly?

```bash
osm2pgsql --output=flex --style=./osm-flex.lua --create --database=<connectionString> <pathToOsmFile>
```

```lua
-- https://osm2pgsql.org/doc/manual.html

-- OSM Flex Import Script for German Geodata
-- Extracts zip code areas, places, and administrative boundaries with multilingual names

-- No need for manual JSON encoding - osm2pgsql handles jsonb conversion automatically

-- Helper function to extract multilingual names following OSM standards
-- Returns: default_name (string), language_names (table)
local function extract_names(object)
    local default_name = object.tags.name  -- Common default name in local language
    local language_names = {}
    
    -- Extract language-specific names (name:lang=*)
    for key, value in pairs(object.tags) do
        if key:match('^name:') then
            local lang = key:sub(6) -- Remove 'name:' prefix
            language_names[lang] = value
        end
    end
    
    -- Skip alt_name, prefix, etc. to maintain consistent language-only structure
    -- These could be stored in separate fields if needed for the application
    
    return default_name, language_names
end


-- Define tables with correct osm2pgsql flex syntax
local postal_areas_table = osm2pgsql.define_table({
    name = 'postal_areas',
    ids = { type = 'area', id_column = 'area_id', create_index = 'primary_key' },
    columns = {
        { column = 'postal_code', type = 'text' },
        { column = 'name', type = 'text' },
        { column = 'names', type = 'jsonb' },
        { column = 'population', type = 'int' },
        { column = 'admin_level', type = 'int' },
        { column = 'area_sqkm', type = 'real' },
        { column = 'geom', type = 'multipolygon', srid = 4326, not_null = true }
    }
})

-- Process relations (for postal code and administrative boundaries)
function osm2pgsql.process_relation(object)
    local postal_code = object.tags.postal_code or object.tags['addr:postcode']
    local boundary = object.tags.boundary
    local relation_type = object.tags.type
    local admin_level = tonumber(object.tags.admin_level)
    
    -- Process postal code boundary relations (exact match to https://github.com/yetzt/postleitzahlen)
    -- Query: relation["type"="boundary"]["boundary"="postal_code"]
    if boundary == 'postal_code' and relation_type == 'boundary' and postal_code then
        local default_name, language_names = extract_names(object)
        local population = tonumber(object.tags.population)
        
        -- Calculate area in square kilometers if possible
        local area_sqkm = nil
        if object.tags['area:km2'] then
            area_sqkm = tonumber(object.tags['area:km2'])
        end
        
        -- Create multipolygon geometry
        local geom = object:as_multipolygon()
        if geom then
            local inserted, message = postal_areas_table:insert({
                postal_code = postal_code,
                name = default_name,
                names = language_names,  -- osm2pgsql will convert table to jsonb automatically
                population = population,
                admin_level = admin_level,
                area_sqkm = area_sqkm,
                geom = geom
            })
            -- Debug failed inserts only
            if not inserted and message then
                print("Failed to insert postal code boundary " .. postal_code .. ": " .. message)
            end
        end
    end
    
    -- Process administrative boundary relations with admin_level 2-8
    -- Query: relation["type"="boundary"]["boundary"="administrative"]["admin_level"~"^[2-8]$"]
    if boundary == 'administrative' and relation_type == 'boundary' and 
       admin_level and admin_level >= 2 and admin_level <= 8 then
        
        local default_name, language_names = extract_names(object)
        local population = tonumber(object.tags.population)
        
        -- Calculate area in square kilometers if possible
        local area_sqkm = nil
        if object.tags['area:km2'] then
            area_sqkm = tonumber(object.tags['area:km2'])
        end
        
        -- Create multipolygon geometry
        local geom = object:as_multipolygon()
        if geom then
            local inserted, message = admin_boundaries_table:insert({
                name = default_name,
                names = language_names,
                admin_level = admin_level,
                population = population,
                area_sqkm = area_sqkm,
                is_lowest_level = false,  -- Will be updated later by the hierarchy creation process
                geom = geom
            })
            -- Debug failed inserts only
            if not inserted and message then
                print("Failed to insert admin boundary " .. (default_name or "unnamed") .. 
                      " (level " .. admin_level .. "): " .. message)
            end
        end
    end
end
```

## What did you expect to happen?

I expect that all postal_code boundaries are imported.


## What did happen instead?

99% of postal_code boundaries are imported successfully. However, some fail because the geometry becomes null. Two postal_code boundaries that fail are "79104" and "79108".

The corresponding OSM data can be found using the following Overpass query:

```
[out:json];
relation["type"="boundary"]["boundary"="postal_code"]["postal_code"="79108"];
out geom;
```

## What did you do to try analyzing the problem?




-- 
Reply to this email directly or view it on GitHub:
https://github.com/osm2pgsql-dev/osm2pgsql/issues/2414
You are receiving this because you are subscribed to this thread.

Message ID: <osm2pgsql-dev/osm2pgsql/issues/2414 at github.com>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.openstreetmap.org/pipermail/tile-serving/attachments/20250909/e6ab46ca/attachment.htm>


More information about the Tile-serving mailing list