Data Schema and Conventions
Coordinates
All coordinates are [longitude, latitude] (GeoJSON RFC 7946): (bb=…),
"$bb", and every Feature.geometry.coordinates in search output. Longitude
is [-180, 180], latitude [-90, 90].
Geometry only. Tools that take a point take it as separate named fields
(from_lat / from_lon), so there is no order to get wrong. The two ordered
exceptions are (around=radiusMetres,lat,lon) and zillow_region's boxes
([minLat, minLon, maxLat, maxLon]) — both lat-first.
Area names
An area is named in snake_case, lower-case, derived from its title:
"New York" → new_york, "Colorado Springs" → colorado_springs. Use
list_areas to discover them — it also returns full_name and the extract's
bounding box, and resolves a human place name to the name a query needs.
An area is a whole state or province, so a city name never matches one.
list_areas(filter="denver") therefore reports match_kind: "none" for the
name lookup and resolves it a second way, as a settlement: the answer carries
resolved_place with the containing area and the city's coordinates, which is
what (around=…) needs. match_kind distinguishes the cases — exact,
substring, resolved_place, none, all — and on none the suggestions
list holds the closest area names, which are frequently unrelated to what
was asked for. Never treat a suggestion as a match.
An area is not a bounding-box filter. Each is its own table built from that
region's source extract, and (area=X) selects from that table. Two
consequences:
- The bbox metadata from
list_areasoverlaps neighbours (Rhode Island's box reaches into Connecticut). Queries do not. - Extracts carry a small border buffer, so features a few km across the line
appear in both adjacent areas. Filter on
addr:stateif you need strict in-state results.
Search result shape
search defaults to a compact shape — cheaper than GeoJSON when you don't
need geometry:
{
"results": [
{
"id": 42,
"osm_id": 123456789,
"osm_type": "node", // node | way | relation
"name": "Joe's Diner",
"lat": 37.77, "lon": -122.41, // bounding-box centroid
"bbox": [-122.42, 37.76, -122.40, 37.78], // omitted for point features
"tags": { "amenity": "restaurant", "name": "Joe's Diner" }
}
],
"count": 1,
"total_count": 1,
"truncated": false
}
"format": "geojson" returns a real FeatureCollection with the feature's
actual geometry where the source has one. If truncated is true, more rows
exist beyond limit and next_offset is the offset to request next.
Common tags
| Tag | Meaning | Example values |
|---|---|---|
amenity |
Category of point of interest | restaurant, cafe, school, hospital |
leisure |
Recreational feature | park, playground, stadium |
tourism |
Tourism feature | museum, hotel, attraction |
shop |
Retail category | supermarket, bakery, clothes |
name |
Display name of this specific feature | "Joe's Diner" |
brand |
Chain or owner, when part of one | Starbucks, Costco |
cuisine |
Cuisine for food-serving amenities | italian, sushi, mexican |
population |
Population count (on place features) |
850000 |
opening_hours |
OSM opening-hours expression | "Mo-Fr 09:00-17:00" |
addr:* |
Structured address components | addr:city, addr:street, addr:postcode |
knowhere://docs/categories maps everyday concepts to the key that holds
them, and says what to confirm before filtering on a value.
brand vs name
brand is the chain; name is this feature's display name. They are often
identical at a chain location, but only brand is reliable for "is this a
chain": chains carry brand / brand:wikidata, independents carry neither.
So "independent coffee shops" is [amenity=cafe][!brand], never a name regex
that lets "Black Rock" or "Beans & Brews" through. Use name=~ for words that
might appear anywhere in a name.
Underlying SQL
Each area gets tables prefixed with its name. Callers never touch these —
search, tag_keys, and tag_values generate the SQL.
{area}_entries— raw OSM data, tags as a JSON blob.{area}_search— FTS5 index over tags, pruning tag filters. Token membership only (detail=none, no positions): it narrows candidates, and exact filtering happens in SQL against{area}_entries.{area}_cells— the spatial index.{area}_entries.idis assigned in grid order, so every cell is a contiguous run of ids and this table records where each run starts and ends, at three grid sizes. That is what prunesbb=,around=,nearandreverse_geocode.{area}_trigram— FTS5 trigram index over the name-ish values (name,name:<lang>,alt_name,brand,operator), so=~on those prunes by substring. Contains on any other tag returns the same rows but scans for them; trigram costs about a token per character, and category-like values are already served by{area}_search.{area}_tag_keys— precomputed key histogram behindtag_keys.
Areas built from a PBF also carry routing graphs, stored as contraction
hierarchies queried by indexed lookup rather than loaded into memory. Geometry
is shared by every profile ({area}_routing_coords, _routing_geom); each
profile has its own graph ({area}_routing_<profile>_nodes, _up, _down),
described by {area}_routing_profiles. A shortcut is a row in _up/_down
carrying a non-NULL via_id rather than a table of its own. Road names are
interned in {area}_road_names and referenced from {area}_routing_edge_ways.
Weights are whole deciseconds. Weights are baked in at contraction, so profiles
cannot share a hierarchy even though they share geometry. A symmetric profile —
a pedestrian one — stores no _down, because it would be a copy of _up.
Related
knowhere://docs/query— query syntax.knowhere://docs/categories— concept to tag key.knowhere://docs/routing— what the routing tables answer.knowhere://docs/errors— error codes.