Coordinates, H3 cells and geohashes
Every layer in Data API — points of interest, public transport, green space, population — is reachable from a coordinate (a point, a radius, a rectangle) or from a set of H3 cells. Points of interest are reachable a third way, by geohash. Same data, same country data, three front doors.
Which door you use is the single biggest structural decision in a Data API integration, and it isn't a matter of taste: it follows from what you already have in your hand.
Coordinate mode
Use coordinate mode when you have a place — a pin someone dropped, a geocoded address, a map viewport. You hand us geometry and we resolve it.
| Shape | Where it lives |
|---|---|
| Point + range | GET /v1/pois/near — lat + lng, then radius_m, k, or both |
| Rectangle | GET /v1/pois/in-bbox — bbox=south,west,north,east, lat-first |
| Text | GET /v1/pois/search — q, ranked by name match |
| Rectangle | /v1/h3/* — the same lat-first bbox |
| Circle | /v1/h3/* — center_lat + center_lng + radius_m |
near takes radius_m ("everything within 500 m"), k ("the nearest 10, however far that
reaches"), or both together to cap a top-N by distance. Send neither and you get a 400 with
code range_required — we won't guess a default radius on your behalf.
Population and transit-coverage are the exceptions to the circle row: they take bbox,
cells, or division only. A circle-only request fails with 400 invalid_input; a circle
alongside a bbox is ignored.
Here is the whole thing end to end: cafés within 500 m of Trafalgar Square, cross-validated only.
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/pois/near?country=GB&lat=51.5074&lng=-0.1278&radius_m=500&category=cafe&min_sources=2&limit=5"
That returns a GeoJSON FeatureCollection — one Feature per POI, each with a point
geometry and the properties described in
How POIs are built.
Cell mode
Use cell mode when you already have a footprint — a travel-time isochrone, a drawn catchment, an administrative area. You hand us cell ids and we look them up.
The unit is an H3 cell: a hexagon from Uber's global grid, addressed
by a hex string like 89195da494bffff. Resolution 9 (~175 m edge) is the default everywhere
and the only one every layer holds; 10 (~65 m) buys street-level detail at roughly 7× the
cell count, and the coarse resolutions roll a city up server-side.
Which resolutions a layer holds is a property of that layer, not of the API. GET /v1/h3
lists them per layer.
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/pois?country=GB&cells=89195da494bffff,89195da4947ffff&resolution=9"
Pass cells as a comma-separated list. A cell we hold no data for is simply absent from the
response — it isn't an error, and it isn't a zero row.
Geohash mode
If your pipeline already speaks geohash, the POI
layers answer to it directly, so you never convert. GET /v1/geohash lists what it serves.
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/geohash/pois?country=GB&geohashes=gcpvj0,gcpvj1"
Two precisions ship: 6 (~1.2 km) and 7 (~150 m). The parameter is called precision,
never resolution — 6 and 7 are also valid H3 resolutions, and one shared name would mean
two different things. Sending resolution is refused rather than ignored: a 400 invalid_input on the query string, a 422 in a POST body.
Geohashes you send win over precision, exactly as cells win over resolution on
/v1/h3/*; the envelope echoes the value actually queried. Every geohash in a request must
be the same length; mixing them is a 400 mixed_precision.
A bbox or a center_lat+center_lng+radius_m works here too, covered at precision —
same centre-of-cell-in-shape rule the H3 covers use:
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/geohash/pois?country=GB&bbox=51.50,-0.15,51.52,-0.10&precision=7"
Precision 6 holds a bbox of roughly 0.76°, precision 7 about 0.13°, before the 10,000-cell
cap answers 400 too_many_cells — which is why precision defaults to 6.
The POST shape takes the same array in a body, for lists past the URL limit:
curl -X POST -H "Authorization: Bearer $POI_API_KEY" -H "Content-Type: application/json" \
-d '{"geohashes": ["gcpvj0", "gcpvj1"], "limit": 100}' \
"$POI_BASE/v1/geohash/pois"
Rows are keyed geohash rather than h3, and the envelope says precision rather than
resolution. Everything else — the category filter, min_sources, per_cell_limit,
breakdown — behaves as it does on /v1/h3/*.
Geohash serves the POI layers only: for land cover, population or transport, start from
/v1/h3. And it is served from the search index, so a deployment reading POIs from a bundle
answers 400 unsupported_backend rather than an empty list that would look like "no POIs
here".
Two knobs cut the response on either cell namespace, and both are worth reaching for on
large cell sets: counts_only=true on the aggregate endpoints drops the per-category rollup
so each cell is just its count (mutually exclusive with breakdown — sending both is a
422), and fields=slim on the records endpoints trims each POI to id, name,
category, lat, lng, source_count.
Picking a mode
The rule is short: a place → coordinate mode, a footprint → cell mode. Don't convert a coordinate to cells yourself first — the endpoints do that internally and more accurately. Reach for geohash mode when geohash is already your addressing scheme; H3 is the mode every layer speaks.
The exception is the one people arrive at late: reach for cell mode whenever you need two layers over the same area, even if you started from a coordinate. Resolve the area to cells once, reuse that list across every layer, and the answers are guaranteed to line up on the same hexagons.
Asking /v1/pois/near for a 500 m circle and /v1/h3/population for a bbox gives you two
different areas, so the ratio between them means nothing. Resolve once, query many.
The shared H3 envelope
Every /v1/h3/* endpoint that returns a list of cells uses the same envelope, whichever mode
you entered by:
{
"country": "GB",
"resolution": 9,
"count": 29,
"limit": 10000,
"truncated": false,
"generated_at": "2026-08-09T13:42:41Z",
"cells": []
}
cells is what differs between layers — the POI layer puts counts and category breakdowns in
it, land cover puts polygons, population puts headcounts and demographics. A layer may add a
field of its own beside it, so the wrapper is worth writing one client-side handler for —
except the POST /v1/h3/population/aggregate rollup, which is its own shape.
Two fields earn attention. truncated tells you limit cut the list short, so a short
response and a complete one are always distinguishable — never infer completeness from
count alone. And resolution echoes what was actually used: when you send cells,
their native resolution wins over any resolution you pass, so read it back rather than
assuming. Transit-coverage is the exception — its cells path always echoes 9, and ids at
any other resolution simply match nothing.
Composing with a travel-time isochrone
- Ask the sibling TravelTime Routing API for the cells reachable in 30 minutes from an origin — that's its h3-fast endpoint, called with your Routing API credentials. It serves 6 through 12, so ask it for resolution 9, the one every Data API layer holds.
- Pass the returned cell ids straight into any Data API layer as
cells=. - Read every layer over the identical footprint — how many cafés, how much park, how many people, how well served by transport.
Step 2 needs no translation because both products address the same H3 grid. Data API never calls the Routing API for you — you hold both sets of credentials and pass the cells across yourself.
Where next
- Quickstart — the shortest path to a first response.
- How POIs are built — what a POI is and how to read its confidence.
- Population & areas — the layer that leans hardest on cell mode.
- API reference — every parameter of every endpoint, generated from the live schema.