Knowhere

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_areas overlaps 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:state if 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.id is 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 prunes bb=, around=, near and reverse_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 behind tag_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.