OpenStreetMap logo OpenStreetMap

German version

Initial Situation

In the forum, a user reports that a road on Crete is not being displayed completely. It quickly becomes clear that the German style is being used for rendering.

Openstreetmap.de operates two tile servers. On both of them, the tiles are faulty. It would be quite a coincidence if this were a hardware issue or a specific data import problem. Therefore, it is very likely that the cause lies within the German style itself.

The only difference between the two road segments is that the visible part contains one additional tag, namely maxspeed:

First Attempt

In the first attempt, I added a tag, namely the surface surface, to the missing segment and forced the German server to re-render the tiles. As a result, the previously missing part of the road appeared. This brought me one step closer, but it is not a solution yet.

Second Attempt

The German style is based on the standard openstreetmap-carto style, which is used on openstreetmap.org for OpenStreetMap maps. It adopts the basic rendering but selectively modifies certain elements to make them more readable for Germans.

I was curious whether the missing road segments were also absent in the original style. Therefore, I rendered them using that style. For this, I used render_single_tile.py and retrieved the necessary information by right-clicking on the corresponding tile at https://tile.openstreetmap.de.

First, I rendered with the German style, where – as expected – the road section was missing:

render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto-de/osm-de.xml --outputfile site/rendersinglefile/1.png

Then with the original style, where the rendered tile was also incomplete:

render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto/mapnik.xml --outputfile site/rendersinglefile/3.png

From my understanding, this made it clear that the error already occurs when importing the OSM data into the database.

Third Attempt with a Misconception

The data import is performed via osm2pgsql. Here too, the Lua script version in the German style, openstreetmap-carto-flex-l10n.lua, was adapted based on openstreetmap-carto-flex.lua from the standard style.

At the beginning, I quickly noticed that roads with additional tags were displayed. Both LUA files – the original version and the modified German version – have since been extended so that certain keys are filtered out via ignore_keys. I also observed that for all road segments that were not displayed, the tags column in the database was empty. Unfortunately, I focused too much on the tags column. The highway field should also have been filled for osm_id 1340291113 – more on that later.

SELECT osm_id, name, highway, ref, tags
FROM planet_osm_line
WHERE osm_id IN (1340291113, 1340291114);
   osm_id   |                  name                   | highway | ref  |       tags       
------------+-----------------------------------------+---------+------+------------------
 1340291113 | Περάματος - Γαζίου - Perámatos - Gazíou |         | ΕΟ90 | 
 1340291114 | Περάματος - Γαζίου - Perámatos - Gazίou | primary | ΕΟ90 | "maxspeed"=>"40"
(2 rows)

Since I read on switch2osm.org that the original version openstreetmap-carto-flex.lua is not yet actively used, I initially investigated in the wrong direction.

I modified the import script so that whenever no tags were present, "dummy"=>"true" is inserted into the tags column:

local function add_linear(table_name, attrs, geom)
    for sgeom in geom:geometries() do
        attrs.way = sgeom

        if next(attrs.tags) == nil then
            attrs.tags.dummy = "true"
        end

        insert_row(table_name, attrs)
    end
end

In the test database, the result now looks like this. That highway and ref were filled is coincidental, as I discovered later:

SELECT osm_id, name, highway, ref, tags
FROM planet_osm_line
WHERE osm_id IN (1340291113, 1340291114);
   osm_id   |                  name                   | highway | ref  |       tags       
------------+-----------------------------------------+---------+------+------------------
 1340291113 | Περάματος - Γαζίου - Perámatos - Gazíou | primary | ΕΟ90 | "dummy"=>"true"
 1340291114 | Περάματος - Γαζίου - Perámatos - Gazíou | primary | ΕΟ90 | "maxspeed"=>"40"
(2 rows)

After this import, render_single_tile.py renders all road segments correctly, both with the original and the German style.

However, my workaround was considered “bad” – rightfully so, since at that point I still did not understand why the problem occurred in the first place.

Fourth Attempt

At this point, I assumed that the mapnik database queries generating the PNG files for the tiles might have issues if the tags column was empty. I had already mentioned that I had previously overlooked the fact that the highway field was also empty.

To keep it short: I could not find a query that selects only road segments that have tags. However, the highway column seemed to be crucial – and this insight guided me back in the right direction.

Fifth Attempt

The function prepare_columns has been worrying me for some time:

for key, value in pairs(object.tags) do
    if tag_map[key] then
        if (key == 'name') and (L10NLANG ~= nil) then
            attrs[key] = gen_l10n_name(object, islinear, iscountry)
        else
            attrs[key] = value
        end
        found_tag = true
    elseif ignore_type and key == 'type' then -- luacheck: ignore 542
        -- do nothing
    elseif keep_tag(key) then
        attrs.tags[key] = value
        found_tag = true
    end
end

Here we iterate over object.tags while simultaneously modifying object.tags in the l10n daemon during the iteration. I hadn’t questioned this before, since only a single tag is temporarily added. In the actual for loop, the same elements remain present.

The Lua manual states: “You should not assign any value to a non-existent field in a table during its traversal.” But if this were truly problematic, far more roads would be rendered incorrectly.

I examined the Lua source code: lua-5.4.8/src/ltable.c contains the function findindex. The error "invalid key to 'next'" would be raised if an element could not be found while iterating over object.tags. However, in my case, no error is logged.

Sixth Attempt

Since I had no other idea, I finally created a copy of object.tags and used it as the index in the for loop:

local keys = {}
for k in pairs(object.tags) do
    keys[#keys+1] = k
end

According to the Lua Manual, this is actually the recommended approach. Now everything works, even with few tags. At this point, I could basically leave it like this: the error is gone, and the code follows the recommendations of the manual.

However, I would still like to understand why the error only occurs for OSM ways with few tags, while those with many tags run stably.

Seventh Attempt

I’m using my faulty Lua script again and creating two OSM export files for quick testing. One of them contains a problematic way:

sudo -u tile wget -O ways.osm "https://overpass-api.de/api/interpreter?data=[out:xml];way(id:1340291113);(._;>;);out body;"

<osm version="0.6" generator="Overpass API 0.7.62.8 e802775f">
<note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note>
<meta osm_base="2025-12-06T16:53:40Z"/>

  <node id="313813429" lat="35.3476725" lon="24.8258749"/>
  ...
  <node id="8959531924" lat="35.3475664" lon="24.8266017"/>
  <way id="1340291113">
    <nd ref="313813429"/>
    ...
    <nd ref="8959531917"/>
    <tag k="highway" v="primary"/>
    <tag k="name" v="Περάματος - Γαζίου"/>
    <tag k="ref" v="ΕΟ90"/>
    <tag k="source:ref" v="Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)"/>
  </way>

</osm>

And an adjacent segment with a way that is not problematic:

sudo -u tile wget -O ways.osm "https://overpass-api.de/api/interpreter?data=[out:xml];way(id:1340291114);(._;>;);out body;"

<note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note>
<meta osm_base="2025-12-06T16:53:40Z"/>

  <node id="313813460" lat="35.3465775" lon="24.8360096"/>
  ...
  <node id="8959531917" lat="35.3465228" lon="24.8358648"/>
  <way id="1340291114">
    <nd ref="8959531917"/>
    ...
    <nd ref="4810699410"/>
    <tag k="highway" v="primary"/>
    <tag k="maxspeed" v="40"/>
    <tag k="name" v="Περάματος - Γαζίου"/>
    <tag k="ref" v="ΕΟ90"/>
    <tag k="source:maxspeed" v="sign"/>
    <tag k="source:ref" v="Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)"/>
  </way>

</osm>

Now I add print() statements to the Lua script. At the beginning of the loop, I print the tag that is currently being processed. Additionally, I print all object.tags together before and after the call to gen_l10n_name, where object.tags is modified.

for key, value in pairs(object.tags) do
    print(string.format("key: %s, value: %s", tostring(key), tostring(value)))

    if tag_map[key] then
        if key == 'name' and L10NLANG ~= nil then

            print("  --> Processing 'name' key with L10NLANG")

            for akey, avalue in pairs(object.tags) do
                print(string.format("    [before] akey: %s, avalue: %s", tostring(akey), tostring(avalue)))
            end

            attrs[key] = gen_l10n_name(object, islinear, iscountry)

            for zkey, zvalue in pairs(object.tags) do
                print(string.format("    [after] zkey: %s, zvalue: %s", tostring(zkey), tostring(zvalue)))
            end
         
            else
              attrs[key] = value
            end
            found_tag = true
        elseif ignore_type and key == 'type' then -- luacheck: ignore 542
            -- do nothing
        elseif keep_tag(key) then
            attrs.tags[key] = value
            found_tag = true
        end
    end

    if not found_tag then
        return nil
    end

    return attrs
end

Using

osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way113.osm

and

osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way114.osm

I can now test quite quickly.

I noticed that for a way with six tags (as in osm_id 1340291114), the order of the tags remains consistent during the loop iteration.

key: ref, value: ΕΟ90
key: highway, value: primary
key: maxspeed, value: 40
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: name, value: Περάματος - Γαζίου
  --> Processing 'name' key with L10NLANG
    [before] akey: ref, avalue: ΕΟ90
    [before] akey: highway, avalue: primary
    [before] akey: maxspeed, avalue: 40
    [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [before] akey: name, avalue: Περάματος - Γαζίου
    [before] akey: source:maxspeed, avalue: sign
    [after] zkey: ref, zvalue: ΕΟ90
    [after] zkey: highway, zvalue: primary
    [after] zkey: maxspeed, zvalue: 40
    [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [after] zkey: name, zvalue: Περάματος - Γαζίου
    [after] zkey: source:maxspeed, zvalue: sign
key: source:maxspeed, value: sign

On a second run, the order can be different. This is also stated in the Lua Manual: “The order in which the indices are enumerated is not specified, even for numeric indices.”

However, during a single loop iteration, the order of object.tags always remains the same – as shown in my print() outputs for key, akey, and zkey.

key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: name, value: Περάματος - Γαζίου
  --> Processing 'name' key with L10NLANG
    [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [before] akey: name, avalue: Περάματος - Γαζίου
    [before] akey: maxspeed, avalue: 40
    [before] akey: source:maxspeed, avalue: sign
    [before] akey: ref, avalue: ΕΟ90
    [before] akey: highway, avalue: primary
    [after] zkey: ref, zvalue: ΕΟ90
    [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [after] zkey: name, zvalue: Περάματος - Γαζίου
    [after] zkey: maxspeed, zvalue: 40
    [after] zkey: source:maxspeed, zvalue: sign
    [after] zkey: highway, zvalue: primary
key: maxspeed, value: 40
key: source:maxspeed, value: sign
key: highway, value: primary

It is quite different with four tags (as in osm_id 1340291113). Here, zkey often changes. When my highway was missing, the situation apparently was like in the following log: Everything started in the order ref, source:ref, name, and highway. Then, when object.tags was modified during the iteration of the name tag, the order changed to source:ref, highway, name, and ref. As a result, highway was not processed in this loop iteration, while ref was processed twice:

key: ref, value: ΕΟ90
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: name, value: Περάματος - Γαζίου
  --> Processing 'name' key with L10NLANG
    [before] akey: ref, avalue: ΕΟ90
    [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [before] akey: name, avalue: Περάματος - Γαζίου
    [before] akey: highway, avalue: primary
    [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [after] zkey: highway, zvalue: primary
    [after] zkey: name, zvalue: Περάματος - Γαζίου
    [after] zkey: ref, zvalue: ΕΟ90
key: ref, value: ΕΟ90

In the next iteration, highway was processed twice.

key: highway, value: primary
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: ref, value: ΕΟ90
key: name, value: Περάματος - Γαζίου
  --> Processing 'name' key with L10NLANG
    [before] akey: highway, avalue: primary
    [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [before] akey: ref, avalue: ΕΟ90
    [before] akey: name, avalue: Περάματος - Γαζίου
    [after] zkey: name, zvalue: Περάματος - Γαζίου
    [after] zkey: ref, zvalue: ΕΟ90
    [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [after] zkey: highway, zvalue: primary
key: ref, value: ΕΟ90
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: highway, value: primary

After that, highway was not processed again:

key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: name, value: Περάματος - Γαζίου
  --> Processing 'name' key with L10NLANG
    [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [before] akey: name, avalue: Περάματος - Γαζίου
    [before] akey: highway, avalue: primary
    [before] akey: ref, avalue: ΕΟ90
    [after] zkey: highway, zvalue: primary
    [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
    [after] zkey: ref, zvalue: ΕΟ90
    [after] zkey: name, zvalue: Περάματος - Γαζίου

With four tags, in my tests it’s a bit of a gamble whether all tags are processed, whereas with six tags everything is stable.

How can this be explained?

Lua tables consist of an array part for positive integer keys and a hash part for all other keys. Our tags, as strings, fall into the hash part. According to my research, the relevant code for adding elements in lua-5.4.8/src/ltable.c looks like this:

if (f == NULL) {  /* cannot find a free place? */
  rehash(L, t, key);  /* grow table */
  luaH_set(L, t, key, value);
  return;
}

This means: if no free hash slot is found, a rehash() is triggered immediately, potentially redistributing the elements.

Why does this happen more often in some tables?

Lua always chooses the hash size as a power of two (2ⁿ):

lsize = luaO_ceillog2(size);
size = twoto(lsize);
Example: 4 elements
ceil(log2(4)) = 2
2^2 = 4
Elements Hash size Free slots
4 4 0

Inserting another element triggers a rehash() and may rearrange the elements.

Example: 6 elements
ceil(log2(6)) ≈ ceil(2.58) = 3
2^3 = 8
Elements Hash size Free slots
6 8 2

Inserting or deleting elements here does not trigger a rehash(). The order of elements remains stable.

Example: 8 elements
ceil(log2(8)) = 3
2^3 = 8
Elements Hash size Free slots
8 8 0

Inserting another element triggers a rehash() again and may rearrange the elements. However, since the table is now larger, the likelihood of a significant portion of elements being “lost” or imported incorrectly is lower.

Email icon Bluesky Icon Facebook Icon LinkedIn Icon Mastodon Icon Telegram Icon X Icon

Discussion

Log in to leave a comment