Choosing an H3 Resolution: Cell Size, Counts and What Each Level Can Show
Problem statement
Every H3 call takes a resolution argument, and the documentation answers the question with a table of average areas. That table is correct and not much help, because the resolution is not really a choice about size. It decides three other things.
- How far a point moves. Once a point is a cell, its position is the cell's centre. At resolution 9 the typical shift is 128 m, and at resolution 5 it is 6.3 km.
- How many points share a cell. Measured on all 13,464,117 GeoNames features, the median occupied cell holds 9 points at resolution 5, 3 at resolution 6 and a single point at resolution 7. By resolution 7, 56.3% of occupied cells hold exactly one point.
- How often near neighbours are split. Of 370,052 point pairs less than 500 m apart, 42.1% fall in different cells at resolution 8 and 81.8% at resolution 9.
Pick a level from the area table alone and you can end up with a map of single points, or a proximity join that misses most of its matches, without any error to warn you.
Quick answer
Print the ladder, then pick the coarsest level whose edge length is still small against the distance your question cares about:
import h3
for res in range(4, 11):
print(f"res {res:2} edge {h3.average_hexagon_edge_length(res, 'm'):>12,.1f} m"
f" area {h3.average_hexagon_area(res, 'km^2'):>14,.6f} km²"
f" cells {h3.get_num_cells(res):>24,}")
res 4 edge 26,071.8 m area 1,770.347654 km² cells 288,122
res 5 edge 9,854.1 m area 252.903858 km² cells 2,016,842
res 6 edge 3,724.5 m area 36.129062 km² cells 14,117,882
res 7 edge 1,406.5 m area 5.161293 km² cells 98,825,162
res 8 edge 531.4 m area 0.737328 km² cells 691,776,122
res 9 edge 200.8 m area 0.105333 km² cells 4,842,432,842
res 10 edge 75.9 m area 0.015048 km² cells 33,897,029,882
Then check it against your own data. The median number of points per occupied cell should be well above one, and the edge should be several times smaller than any distance you intend to reason about.
Step-by-step solution
1. Start from the length scale of the question
State the distance the analysis is about before looking at any table. "Which neighbourhoods have the most cafés" is a question at a few hundred metres. "How does settlement density vary across a continent" works at tens of kilometres.
Resolutions 8 and 9 (edges of 531 m and 201 m) fit the first question. Resolutions 3 to 5 (69 km to 9.9 km) fit the second. A useful rule of thumb is an edge two to five times smaller than the distance you want to see. Go finer than that and each feature is split across several cells; go coarser and the pattern disappears inside a single cell.
2. Measure how far the grid moves your points
Assigning a point to a cell replaces its coordinates with the cell. Any later distance, map or join works with the cell centre. The shift can be measured directly. This is 235,735 GeoNames places with population of 500 or more:
median_m p95_m max_m
3 44131.2 63026.1 74877.5
5 6261.9 8989.4 10792.0
7 897.1 1285.0 1534.0
9 128.0 183.5 219.2
11 18.3 26.2 31.3
13 2.6 3.7 4.5
The median shift is about 0.64 edge lengths and the maximum is slightly above one edge. So "resolution 9 is accurate to 200 m" is a fair summary, and "resolution 9 is accurate to 128 m" is true only for the typical point.
3. Count points per occupied cell across levels
A grid finer than the data supports shows where individual points are rather than how dense they are. Sweep several resolutions over the real dataset. Measured on all 13,464,117 GeoNames features:
occupied median p90 max single_pct
res
3 17210 124.0 2203.3 33975 14.2
4 87169 35.0 412.0 12544 10.2
5 451015 9.0 74.0 6716 14.4
6 1963288 3.0 15.0 2196 29.7
7 5798146 1.0 5.0 1163 56.3
8 9986989 1.0 2.0 360 79.8
9 12159083 1.0 1.0 360 92.4
10 12868451 1.0 1.0 360 96.5
For a global density map, resolution 5 is the finest level where a typical cell still holds a count rather than one point. For one city with dense data, resolution 8 or 9 can reach the same point. The sweep is cheap, 9.3 s for eight levels in DuckDB, so run it on your own data rather than borrowing these numbers.
4. Check how often near pairs end up in different cells
If the grid will be used as a join key or for proximity, the question is how often two nearby points are placed in different cells. Measured on 326,856 GeoNames places and sites in Great Britain, France and Germany:
33,026 pairs within 100 m -> % split by resolution: {7: 3.5, 8: 9.4, 9: 21.4, 10: 53.1}
370,052 pairs within 500 m -> % split by resolution: {6: 6.3, 7: 16.6, 8: 42.1, 9: 81.8}
A same-cell match at resolution 9 would find fewer than one in five of the pairs within 500 m. You can pick a coarser level, or keep the fine level and search neighbouring cells as well. The neighbour search is covered in the k-ring guide.
5. Allow for the area spread within one level
The average area is an average. All 288,122 resolution-4 cells range from 896.6 km² to 2,136.0 km², a ratio of 2.38, and only 51.4% are within ±10% of the mean. At resolution 7 the cell over London is 4.615 km² and the one over Sydney 6.209 km².
So when cells are compared, divide counts by h3.cell_area(cell, "km^2") rather than by the table value. Resolution changes how big the cells are on average, but no level makes them equal.
6. Check the output size before you commit
Occupied cells are the rows of your result. On GeoNames, going from resolution 5 to resolution 7 increases the output from 451,015 rows to 5,798,146. Resolution 9 gives 12,159,083 rows, nearly one per input point. If each row later becomes a polygon, that is the memory bill.
7. Put the resolution in the column name
Store cells as h3_7, not h3. Cells at different resolutions are different integers, and a join between a resolution-7 table and a resolution-8 table matches nothing and raises no error. With the level in the column name, the mismatch is visible before the join runs.
Code examples
Example 1 — a resolution table for a study region
import h3
import pandas as pd
def resolution_table(region_km2=None, resolutions=range(0, 16)):
"""Edge, average area and expected cell count for each level."""
rows = []
for res in resolutions:
area = h3.average_hexagon_area(res, "km^2")
row = {"res": res,
"edge_m": round(h3.average_hexagon_edge_length(res, "m"), 1),
"area_km2": area,
"cells_on_earth": h3.get_num_cells(res)}
if region_km2:
row["cells_in_region"] = round(region_km2 / area)
rows.append(row)
return pd.DataFrame(rows).set_index("res")
For a study region of 243,610 km², roughly the area of the United Kingdom:
edge_m area_km2 cells_on_earth cells_in_region
res
4 26071.8 1770.347654 288122 138
5 9854.1 252.903858 2016842 963
6 3724.5 36.129062 14117882 6743
7 1406.5 5.161293 98825162 47199
8 531.4 0.737328 691776122 330396
9 200.8 0.105333 4842432842 2312771
10 75.9 0.015048 33897029882 16189398
The last column is an upper bound on the output of a full fill. It is a quick check that a national map at resolution 10 means 16 million polygons.
Example 2 — an occupancy sweep over the real data
import duckdb
import pandas as pd
def occupancy_sweep(source, resolutions=range(3, 11), lat="lat", lon="lon", threads=4):
"""Points per occupied cell at each level, computed in one pass per level."""
con = duckdb.connect()
con.execute("load h3")
con.execute(f"set threads = {threads}")
con.execute("set enable_progress_bar = false")
rows = []
for res in resolutions:
rows.append(con.execute(f"""
with g as (select h3_latlng_to_cell({lat}, {lon}, {res}) as cell, count(*) as n
from {source} group by 1)
select {res}, count(*), median(n), quantile_cont(n, 0.9), max(n),
round(100 * avg((n = 1)::int), 1)
from g""").fetchone())
return pd.DataFrame(rows, columns=["res", "occupied", "median", "p90", "max",
"single_pct"]).set_index("res")
The call occupancy_sweep("read_parquet('geonames_all.parquet')") produced the table in step 3 in 9.3 s. It needs the DuckDB h3 community extension (install h3 from community once per machine).
Example 3 — the quantisation error at several levels
import h3
import numpy as np
import pandas as pd
def quantisation_error(lat, lon, resolutions=(5, 7, 9, 11)):
"""Distance from each point to the centre of its cell, in metres."""
lat = np.asarray(lat, dtype=float)
lon = np.asarray(lon, dtype=float)
out = {}
for res in resolutions:
centres = np.array([h3.cell_to_latlng(h3.latlng_to_cell(a, b, res))
for a, b in zip(lat.tolist(), lon.tolist())])
p1, p2 = np.radians(lat), np.radians(centres[:, 0])
dlon = np.radians(centres[:, 1] - lon)
h = np.sin((p2 - p1) / 2) ** 2 + np.cos(p1) * np.cos(p2) * np.sin(dlon / 2) ** 2
d = 2 * 6_371_008.8 * np.arcsin(np.sqrt(h))
out[res] = {"median_m": np.median(d), "p95_m": np.percentile(d, 95), "max_m": d.max()}
return pd.DataFrame(out).T.round(1)
If a report says "locations were generalised to H3 resolution 9", this is the function that turns that into a number a reader can use: a median of 128 m and a maximum of 219 m.
Explanation
Why each level is seven times smaller
H3 has aperture 7. Each cell has seven children, a centre child and six around it, rotated so that they tile the plane. Area therefore falls by a factor of 7 per level, and edge length by √7 ≈ 2.646. Measured from the averages, the area ratio between neighbouring levels is 7.0000 from resolution 5 onwards, and the edge ratio stays between 2.645 and 2.649. The coarsest levels are slightly off (7.15 from resolution 0 to 1) because the twelve pentagons make up a larger share of so few cells.
The consequence is that H3 has no halfway step. If resolution 7 (5.16 km²) is too coarse and resolution 8 (0.74 km²) too fine, the cell you want is around 1.95 km², the geometric midpoint, and no level gives you that. You choose which way to err.
Why the average area hides a twofold spread
H3 projects each face of an icosahedron onto the sphere, and cells near the middle of a face come out larger than cells near its corners. The spread follows position on the icosahedron, not latitude. Measured over every resolution-5 cell, the mean area is 249.7 km² between 0° and 15° of latitude and 265.4 km² between 75° and 90°. The extremes are 127.8 km² for the pentagons and 305.1 km² for the largest hexagons.
Changing resolution keeps the pattern: every level has the same roughly twofold range, repeated in smaller cells.
Why the occupancy collapses so abruptly
Dividing the area by seven divides the expected count by seven too, but real points are not spread evenly. Resolution 5 to resolution 6 cuts the median from 9 to 3. From 6 to 7 it drops to 1, and the share of single-point cells almost doubles, from 29.7% to 56.3%.
Beyond that point the grid stops summarising: a cell becomes one point, and the map shows the sampling pattern of the source. This is the modifiable areal unit problem in a regular form. The grid is regular, and your choice of resolution still shapes the answer.
Why finer is worse for proximity
It is tempting to think a finer grid is always more accurate. For position that is true: the shift falls from 897 m at resolution 7 to 128 m at resolution 9. For relationships between points it is false, because every cell boundary splits pairs that are close together. Smaller cells mean more boundaries per kilometre, and more split pairs.
The measured split rate for pairs within 500 m rises from 16.6% at resolution 7 to 81.8% at resolution 9. For anything about "near", choose the level for the neighbourhood you will search, not for the precision of a single point.
Edge cases or notes
- Resolution 15 is not "exact". Its edge is 0.6 m, which is still coarser than survey-grade coordinates, and at 570 quadrillion cells the level is really meant for indexing, not analysis.
- One level for a global map is a compromise. Dense cities and empty deserts want different levels. The usual answers are a coarser level with a density measure, or compacted mixed-resolution cells for storage.
- The table's area is an average over the sphere, not the area of your cells. Use
h3.cell_areaper cell whenever areas enter a calculation. - The pentagons are rare but real. There are twelve at every level, all at sea at resolution 5, each with five neighbours instead of six.
- Coarse levels differ from the ×7 rule. Resolution 0 to 1 is a factor of 7.15 in area, not 7.0.
- Changing resolution changes every cell identifier. Parent and child are related, but they are different integers, and the hierarchy does not nest geometrically.
- The occupancy sweep describes your data, not H3. A sensor network and a gazetteer of the same country need different levels.
- Record the level with the result. A shared table of cell counts with no resolution attached is ambiguous.
Internal links
- H3 explained: how a hexagonal grid indexes the whole Earth — the icosahedron and the aperture-7 design behind the table
- Discrete global grids explained — how H3's levels compare with S2, geohash and quadkeys
- How to assign points to H3 cells in Python — the step that applies the resolution
- How to aggregate points into H3 hexagons and map them — putting the chosen level on a map
- H3 hierarchy explained — what happens when you move between levels
- How to use H3 neighbours for k-ring smoothing and buffers — the answer to split pairs
- Fixing an H3 map that misleads — unequal areas and empty cells on a finished map
- The modifiable areal unit problem explained — why the choice changes the result
FAQ
What H3 resolution should I use?
Choose the coarsest level whose edge is two to five times smaller than the distance your question cares about. Then confirm that the median number of points per occupied cell is well above one. On global GeoNames that pointed to resolution 5; for one dense city it is often 8 or 9.
How big is an H3 cell at resolution 9?
On average 0.105 km², with an edge of 200.8 m. Individual cells vary by roughly a factor of two around that average depending on where they fall on the icosahedron.
How accurate is a point stored as an H3 cell?
The typical point is about 0.64 edge lengths from its cell centre, and the worst case is about one edge. Measured at resolution 9, that is a median of 128 m and a maximum of 219 m.
Why are most of my cells holding only one point?
The resolution is finer than your data's density. On 13.46 million GeoNames features, 56.3% of occupied cells held one point at resolution 7 and 92.4% at resolution 9. Go coarser until the median rises.
Is a finer resolution always better?
No. It locates single points more precisely, but it splits more nearby pairs across cell boundaries. At resolution 9, 81.8% of pairs within 500 m were in different cells.
Do all cells at one resolution have the same area?
No. Resolution-4 cells range from 896.6 to 2,136.0 km², and only about half are within 10% of the mean. Divide by each cell's own area when comparing densities.