Skip to content

Make POI tag ranking deterministic and rank-aware#889

Open
Symmetricity wants to merge 2 commits into
systemed:masterfrom
Symmetricity:fix/poi-tag-order
Open

Make POI tag ranking deterministic and rank-aware#889
Symmetricity wants to merge 2 commits into
systemed:masterfrom
Symmetricity:fix/poi-tag-order

Conversation

@Symmetricity
Copy link
Copy Markdown
Contributor

@Symmetricity Symmetricity commented May 13, 2026

This PR is AI generated.

Make POI tag ranking deterministic and rank-aware

GetPOIRank() returns the first matching POI tag group. It currently iterates
poiTags with pairs(), but Lua does not specify table traversal order for
non-array tables. That means an object with more than one matching POI tag can
receive different class, subclass, and rank values depending on Lua table
iteration order.

OpenMapTiles ranks POIs by class priority in ascending order:

  • layers/poi/class.sql defines poi_class_rank(class)
  • layers/poi/poi.sql orders POIs with poi_class_rank(...) ASC

Tilemaker's OpenMapTiles profile mirrors that priority with poiClassRanks.
Use an explicit poiTagKeys array and iterate it with ipairs(), but do not
stop at the first matching key. Instead, evaluate all explicit matching POI tag
groups and return the candidate with the lowest numeric poiClassRanks value.
The explicit key order remains the deterministic tie-break for equal ranks.
OpenMapTiles does not define a secondary equal-rank ordering in layer_poi(),
so this tie-break is tilemaker's deterministic profile order rather than a
claim about SQL result order.

Apply the same change to both OpenMapTiles profiles,
process-openmaptiles.lua and process-debug.lua.

Examples

An object with both of these tags has two valid POI matches:

amenity=school
tourism=attraction

Before this change, whichever key pairs(poiTags) returned first determined
the output. The object could be classified from either matching tag group.

After this change, both candidates are evaluated and the OpenMapTiles-style
rank priority is used:

4:attraction:attraction

That matches the profile rank table because attraction has a higher priority
than school.

For the sports pitch case from the visual evidence:

leisure=pitch
sport=soccer

The previous deterministic-only version consistently selected the first
profile key:

25:pitch:soccer

This version selects the better-ranked class:

8:stadium:soccer

For equal ranks, the explicit key order is still used as the tie-break. For
example:

leisure=sports_centre
sport=tennis

returns the first equal-rank candidate in process-openmaptiles.lua:

25:leisure:sports_centre

Reproduce

Load the OpenMapTiles process file and evaluate POIs that have multiple
matching tag groups:

lua - <<'LUA'
dofile('resources/process-openmaptiles.lua')
local cases = {
  { name='school_attraction', tags={ amenity='school', tourism='attraction' } },
  { name='pitch_soccer', tags={ leisure='pitch', sport='soccer' } },
  { name='sports_centre_tennis_tie', tags={ leisure='sports_centre', sport='tennis' } },
}
for _,case in ipairs(cases) do
  function Find(k) return case.tags[k] or '' end
  local rank, class, subclass = GetPOIRank()
  print(case.name .. ' ' .. rank .. ':' .. class .. ':' .. subclass)
end
LUA

Expected output:

school_attraction 4:attraction:attraction
pitch_soccer 8:stadium:soccer
sports_centre_tennis_tie 25:leisure:sports_centre

The same ranking issue and fix apply to resources/process-debug.lua.

Testing

  • git diff --check origin/master..HEAD
  • luac -p resources/process-openmaptiles.lua resources/process-debug.lua
  • Focused Lua checks for GetPOIRank() in both process files:
    • amenity=school + tourism=attraction
    • leisure=pitch + sport=soccer
    • leisure=sports_centre + sport=tennis

Related Issues And PRs

I did not find an existing upstream issue, PR, or discussion that directly
reports this pairs(poiTags) ordering issue.

Related POI context:

GetPOIRank returned the first matching POI tag group, but it iterated poiTags with pairs(). Lua does not specify table traversal order, so objects matching multiple POI groups could receive different class and rank values across runs or platforms.

Iterate an explicit POI key list with ipairs() so the profile priority order is stable, and keep the subclass temporary local to the ranking function.
@Symmetricity
Copy link
Copy Markdown
Contributor Author

I added visual checks for the determinism issue.

For this comparison I generated two local upstream outputs from the same binary,
same upstream pairs(poiTags) profile, same input, and same command. The same
multi-tag POIs resolved to different POI tags between those two upstream runs.
The PR output resolves those same objects by the explicit profile order, so the
selected tag is stable between runs.

I also generated the PR output twice for the same objects; the selected POI tags
matched between the two PR runs.

The screenshots use a local debug_osm_id attribute only to match generated
POIs back to source OSM ways; that attribute is not part of this PR.

Sportsplatz Rheinwiese

Source tags include leisure=pitch and sport=soccer.

  • Upstream run A: class=stadium, subclass=soccer, rank=8
  • Upstream run B: class=pitch, subclass=soccer, rank=25
  • PR result: class=pitch, subclass=soccer, rank=25

Sportsplatz Rheinwiese comparison

Schwimmbad Mühleholz

Source tags include leisure=sports_centre and sport=swimming.

  • Upstream run A: class=swimming, subclass=swimming, rank=25
  • Upstream run B: class=leisure, subclass=sports_centre, rank=25
  • PR result: class=leisure, subclass=sports_centre, rank=25

Schwimmbad Mühleholz comparison

Tennishalle Schaan

Source tags include leisure=sports_centre and sport=tennis.

  • Upstream run A: class=sport, subclass=tennis, rank=25
  • Upstream run B: class=leisure, subclass=sports_centre, rank=25
  • PR result: class=leisure, subclass=sports_centre, rank=25

Tennishalle Schaan comparison

@Symmetricity
Copy link
Copy Markdown
Contributor Author

I think this one will be the most controversial. The issue at play that this PR fixes is that currently POI tagging isn't deterministic - on one run it will be right, on others wrong. What this fixes is that it will be consistent - either wrong all the time, or right all the time.
A separate fix will be needed to make the rankings actually correct - so that in the stadium example above, it will always return the rank 8 tag instead of the rank 25.
This PR just makes that fix easier in the future.

OpenMapTiles ranks POI classes by ascending class priority, but the profile previously stopped at the first matching POI key. Even after making that iteration deterministic, multi-tag POIs could consistently select a lower-priority class.

Evaluate all explicit POI tag matches and keep the lowest numeric rank, using the explicit key list only as the deterministic equal-rank tie-break. This keeps the profile stable while matching the OpenMapTiles priority model more closely.

Co-authored-by: Codex <noreply@openai.com>
@Symmetricity Symmetricity changed the title Make POI tag ranking deterministic Make POI tag ranking deterministic and rank-aware Jun 4, 2026
@Symmetricity
Copy link
Copy Markdown
Contributor Author

Okay, I've fixed my own concern here now. This will now make it deterministic & rank aware, so it will always return the highest ranked (most important) tag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant