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.

Grid of H3 resolutions 4 to 10 with edge length, average area and the scale of pattern each level suits.
Each step down divides the area by seven and the edge by about 2.65, so two levels either side of a good choice are very different maps.

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.

Bar chart of the share of occupied H3 cells holding a single GeoNames point at resolutions 4 to 9.
The curve turns sharply between 6 and 8: that is where this dataset stops supporting densities.

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.

Bar chart of the share of point pairs within 500 metres that fall in different H3 cells at resolutions 6 to 9.
Pairs within 100 m fare better, 21.4% split at resolution 9, but 53.1% at resolution 10.

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_area per 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.

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.