GSoC midterm: category support in Nominatim, first half report
Posted by Agasta07 on 12 July 2026 in English.Midterm is here, so this is a proper writeup of what’s landed so far, how the pieces fit together, and what the second half looks like. If you just want the code: everything described here merged in #4106.
Quick context for new readers: Nominatim identifies every place with a
single class/type pair derived from OSM tags. An object tagged both
tourism=hotel and amenity=restaurant becomes two rows in the
database. Admin boundaries need admin_level special-casing everywhere.
And there’s no way to express “wheelchair accessible cafe” at all. This
project adds a proper category system to fix that at the database level.
Here’s the state of things at the halfway mark.
the data model: ltree[]
Categories are stored as an ltree[] column on place and placex, indexed with GiST. Each category is a dot-separated
hierarchical path:
{osm.amenity.restaurant, osm.tourism.hotel}
I benchmarked this against TEXT[] with GIN during community bonding on a full planet import (~3 days to set up, worth it). The TEXT[]
approach is what Photon effectively does at the OpenSearch level:
pre-expand every prefix at index time (osm.amenity.restaurant also
stores osm.amenity) and match with array overlap. It works, but you
pay storage for every prefix of every category on every row, and the
expansion logic lives in application code.
ltree understands hierarchy natively:
-- all amenities: restaurants, cafes, bars, everything below
WHERE categories @> 'osm.amenity'::ltree
-- exact match
WHERE 'osm.amenity.restaurant'::ltree = ANY(categories)
-- multi-value alternation (waterway checks)
WHERE categories ~ 'osm.waterway.river|stream|canal|drain|ditch'::lquery
Less storage, less code, and the query planner gets a real index to work with.