Population & areas
Every res-9 H3 cell carries a population count, the OSM admin breadcrumb that locates it (United Kingdom → England → Greater London → Camden → …), and — where a country publishes small-area census — a demographics block. This is the data-layer side of the H3 Data API: it answers "how many people live here, and who are they?".
Two companion pieces sit alongside it:
- Transit coverage (
/v1/h3/transit-coverage) — the share of a cell's population within a 15-minute walk of a public-transport station, and the same rolled up per admin area. - Areas (
/v1/divisions) — the administrative-boundary hierarchy. You browse the tree and ask any division for its H3 cells, then feed those cells back into any H3 layer.
Where the numbers come from
- Population is WorldPop (2020, 100 m grid) summed into each res-9 cell. It exists for every inhabited cell, everywhere we ship a country.
- Demographics come from each country's official small-area census and small-area income
product (for GB: ONS Census 2021 output areas + modelled MSOA net household income). A cell's
rates are the population-correct apportionment of the source units that overlap it. Cells with
no census coverage carry population but a
nulldemographics block — never a fabricated zero.
Querying the population layer
GET /v1/h3/population uses the same geographic-input contract as the rest of /v1/h3/* —
supply exactly one of:
bbox=south,west,north,east— every indexed cell whose centroid falls inside the box. Addmin_population=1to drop empty cells (the common case for a viewport heatmap).cells=<csv>— look up a specific set of cells. A single id is just a one-element list; an unindexed cell is simply absent from the result (no 404).division=<id>— every cell inside an admin division. The id comes from the Areas API (/v1/divisions).
# Population for the cells of a London viewport
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/population?country=GB&bbox=51.50,-0.15,51.55,-0.10&min_population=1"
# Everyone inside a division (Areas API id)
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/population?country=GB&division=R51800"
Every response is the shared H3 envelope: { country, resolution, count, limit, truncated, generated_at, cells }. limit caps at 10,000 cells; add fields=slim to trim each row to
h3 + population.
Rolling an area up
To get one area-level summary instead of per-cell rows — population sums plus population-weighted mean rates — POST the cell-set to the aggregate action. This is what powers catchment analytics: a travel-time isochrone, a drawn polygon, or a division all reduce to a set of H3 cells.
Send exactly one area: cells (up to 100,000 ids, read in chunks server-side) or division_id.
Use the id for anything county-scale — the footprint is resolved server-side, so you never hit the
10,000-cell truncation on GET /v1/divisions/{id}/cells.
curl -X POST -H "Authorization: Bearer $POI_API_KEY" \
-H 'content-type: application/json' \
"$POI_BASE/v1/h3/population/aggregate?country=GB" \
-d '{"cells": ["89195da4a93ffff", "89195da4a97ffff"]}'
# A whole division, without shipping its cells
curl -X POST -H "Authorization: Bearer $POI_API_KEY" \
-H 'content-type: application/json' \
"$POI_BASE/v1/h3/population/aggregate?country=GB" \
-d '{"division_id": "R51800"}'
Transit coverage
GET /v1/h3/transit-coverage returns the covered cells (those within a 15-min walk of a station)
for a bbox, cells, or division, with their covered population, station/line counts, and
serving modes. The per-area headline — coverage % of a division's population — is at
/v1/h3/transit-coverage/divisions.
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/transit-coverage?country=GB&bbox=51.48,-0.20,51.56,-0.05&res=9"
To draw the gap rather than infer it, add include_uncovered=true: populated cells no station
reaches come back with covered: false and their uncovered_pop, and at res=6/7 every
parent hex carries the summed uncovered population beneath it — the country-zoom view, which no
join against the res-9 population layer can produce. limit defaults to the 10,000 cap under
include_uncovered; at res=7 a whole-country box still holds more populated parent hexes than
that, so it comes back truncated: true — narrow the box; res=6 fits in one page.
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/transit-coverage?country=GB&bbox=50.0,-6.0,58.7,1.8&res=7&include_uncovered=true"
Areas (the boundary hierarchy)
/v1/divisions is a tree of administrative polygons, not a per-cell layer — so it lives next to
the H3 layers rather than under /v1/h3. Browse it (/v1/divisions, /{id}, /{id}/children)
and ask any division for its cells (/v1/divisions/{id}/cells), then pass those cells into the
population, transit-coverage, POI, or land-cover layers.
Related
- How POIs are built — the other data layer most people pair with this.
- Public transport — the transport graph behind transit coverage.
- API reference — Population & census, Transit coverage, and Areas.