H3 Explained: How a Hexagonal Grid Indexes the Whole Earth
Problem statement
H3 is simple to use: h3.latlng_to_cell(lat, lng, 9) returns 89194ad14c3ffff, and that string can go straight into a groupby. The trouble is what the call hides. People treat the result as a regular hexagon of fixed size that sits exactly inside its parent. Several of those assumptions turn out false when measured:
- Cell area varies 1.98× within a resolution, and the variation has nothing to do with latitude. The largest resolution-5 hexagon (305.1 km²) is at 74.9° N, and the smallest (153.8 km²) is at 64.7° N, beside a pentagon off the Norwegian coast.
- Not every cell is a hexagon. Each resolution has 12 pentagons. At resolution 5, 0.73% of cells have 7, 8 or 10 vertices when drawn.
- Bad input does not always raise.
latlng_to_cell(95, 10, 5)quietly returns a cell at 84.96° N, 170.54° W, on the far side of the pole.
None of these are bugs. Each one follows from how the grid is built, and knowing the construction tells you which of them your analysis has to handle.
Quick answer
import h3
cell = h3.latlng_to_cell(51.5007, -0.1246, 9)
print(cell, h3.str_to_int(cell))
print("resolution", h3.get_resolution(cell), "base cell", h3.get_base_cell_number(cell))
print("centre", h3.cell_to_latlng(cell))
print("area", round(h3.cell_area(cell, "km^2"), 4), "km²")
print("pentagon?", h3.is_pentagon(cell), "class III?", h3.is_res_class_III(cell))
89194ad14c3ffff 617438095025111039
resolution 9 base cell 12
centre (51.49987251358107, -0.12606627049286803)
area 0.0942 km²
pentagon? False class III? True
H3 in one paragraph: an icosahedron projected onto the sphere gives 122 base cells. Each is subdivided in steps of seven, 15 times over. A cell's index is its base cell plus one digit from 0 to 6 per step, packed into 64 bits. The hexadecimal string and the integer are the same index. This Westminster cell covers 0.0942 km², against the resolution-9 average of 0.1053 km².
Step-by-step solution
1. Start from an icosahedron
H3 places an icosahedron (20 triangular faces, 12 vertices) around the Earth. It projects each face onto the sphere with a gnomonic projection centred on that face. Of the regular polyhedra, the icosahedron has the most faces, which means the least distortion per face.
Each face carries a triangular lattice. The hexagons at resolution 0 give 122 base cells: 110 hexagons and 12 pentagons, with one pentagon at each icosahedron vertex. H3 orients the icosahedron so that all 12 vertices fall in the ocean. Measured against Natural Earth land polygons, not one resolution-5 pentagon centre is on land.
2. Subdivide in steps of seven
Every finer resolution divides each cell's area by about seven. A hexagon has seven children: one in the centre and six around it. A pentagon has six. The cell count per resolution follows directly:
print(h3.get_num_cells(5), 2 + 120 * 7 ** 5) # 2016842 2016842
Seven small hexagons cannot rebuild a larger hexagon exactly, so the child lattice is rotated about 19.1° from its parent's. Resolutions then alternate between two orientations: even ones are Class II and odd ones are Class III. is_res_class_III is true for 1, 3, 5 … 15.
What the rotation implies for parents and children (children overhang the parent's outline) has its own guide: H3 hierarchy explained.
3. Read the 64-bit index
The index of 89194ad14c3ffff in binary:
0000100010010001100101001010110100010100110000111111111111111111
| Bits | Field | Value here |
|---|---|---|
| 1 | reserved | 0 |
| 4 | mode (1 = cell) | 1 |
| 3 | reserved for edges and vertices | 0 |
| 4 | resolution | 9 |
| 7 | base cell | 12 |
| 45 | 15 digits of 3 bits | 5 1 2 6 4 2 4 6 0, then 7s |
A parent is the same number with digits past its resolution set to 7 and the resolution field lowered. That is why cell_to_parent needs no geometry at all. It also explains the look of the strings: coarse cells end in long runs of f.
4. Account for the pentagons
Twelve pentagons exist at every resolution, always centred at the same 12 places. They have five neighbours instead of six, six children instead of seven, and they break some grid algorithms. grid_distance raised H3FailedError for 360 of 1,600 cell pairs around a resolution-8 pentagon.
In practice they rarely matter for land data. Only 31 of 13,464,117 GeoNames points fell inside a resolution-5 pentagon. At resolution 9 pentagons are 12 cells out of 4.8 billion. Code that walks the grid still has to survive them.
5. Do not expect area to follow latitude
Unlike a degree grid, H3 cells do not shrink towards the poles. They change size with their distance from the centre of their icosahedron face, where the gnomonic projection distorts least. Every cell at resolution 5, grouped by latitude band:
|lat| 0–15° mean 249.7 km²
|lat| 15–30° mean 257.3
|lat| 30–45° mean 255.5
|lat| 45–60° mean 247.0
|lat| 60–75° mean 251.4
|lat| 75–90° mean 265.4
The band means barely move, while individual cells range from 153.8 to 305.1 km². Only 51.3% of cells are within ±10% of the mean. For a density map, divide by cell_area(cell), not by the nominal area of the resolution.
6. Expect more than six vertices at odd resolutions
A Class III cell that straddles an icosahedron edge picks up extra vertices where the edge crosses it. Counting cell_to_boundary for every cell:
res 4 (Class II): 288,122 cells — 5 or 6 vertices only
res 5 (Class III): 2,016,842 cells — 11,760 with 7, 2,910 with 8, 12 with 10
Code that assumes six vertices, such as a fixed-shape NumPy array, breaks on 0.73% of resolution-5 cells and on a third of resolution-1 cells. See turning cells into polygons for a builder that handles any vertex count.
7. Validate coordinates before indexing
latlng_to_cell raises H3LatLngDomainError for NaN or infinite values. It does not raise for a latitude of 95, or for a longitude of 200. It wraps them:
(95, 10) -> 85056d63fffffff centre (84.958, -170.545)
(150, 10) -> 85470a4ffffffff centre (29.949, -169.941)
(51.5, 200) -> 85228307fffffff centre (51.465, -159.988)
A longitude of 200 wrapping to −160 is harmless. A latitude of 95 wrapping over the pole means the columns were swapped, and H3 will not tell you. With GeoNames' columns swapped, all 13,464,117 rows still indexed without an error. The median point moved 4,201 km.
Code examples
Example 1 — decode an index into its fields
import h3
def decode_h3(cell):
"""Split an H3 cell index into the fields packed into its 64 bits."""
i = h3.str_to_int(cell) if isinstance(cell, str) else int(cell)
res = (i >> 52) & 0xF
digit = lambda r: (i >> (3 * (15 - r))) & 0b111
return {
"mode": (i >> 59) & 0xF, # 1 = cell
"resolution": res,
"base_cell": (i >> 45) & 0x7F, # 0–121
"digits": [digit(r) for r in range(1, res + 1)],
"padding_all_7": all(digit(r) == 7 for r in range(res + 1, 16)),
}
cell = h3.latlng_to_cell(51.5007, -0.1246, 9)
print(decode_h3(cell))
print(decode_h3(h3.cell_to_parent(cell, 5)))
{'mode': 1, 'resolution': 9, 'base_cell': 12, 'digits': [5, 1, 2, 6, 4, 2, 4, 6, 0], 'padding_all_7': True}
{'mode': 1, 'resolution': 5, 'base_cell': 12, 'digits': [5, 1, 2, 6, 4], 'padding_all_7': True}
The parent's digits are a prefix of the child's. h3.get_index_digit(cell, r) returns the same digits, and the library functions are what production code should call. Decoding by hand is for seeing why two indexes share a prefix, or for finding the base cell in a SQL engine that only has bit operators.
Example 2 — the area profile of a whole resolution
import numpy as np
import h3
def area_profile(res, band=15):
"""Area of every cell at one resolution, summarised by latitude band."""
cells = [c for base in h3.get_res0_cells() for c in h3.cell_to_children(base, res)]
area = np.array([h3.cell_area(c, "km^2") for c in cells])
lat = np.abs([h3.cell_to_latlng(c)[0] for c in cells])
hexes = ~np.array([h3.is_pentagon(c) for c in cells])
print(f"res {res}: {len(cells):,} cells, hexagons {area[hexes].min():.1f}–"
f"{area[hexes].max():.1f} km² ({area[hexes].max() / area[hexes].min():.2f}×), "
f"pentagons {area[~hexes].min():.1f} km²")
for lo in range(0, 90, band):
m = (lat >= lo) & (lat < lo + band)
print(f" |lat| {lo:2}–{lo + band:2}° mean {area[m].mean():7.1f} "
f"min {area[m].min():7.1f} max {area[m].max():7.1f}")
return cells, area
res 5: 2,016,842 cells, hexagons 153.8–305.1 km² (1.98×), pentagons 127.8 km²
|lat| 0–15° mean 249.7 min 127.8 max 305.1
|lat| 15–30° mean 257.3 min 127.8 max 305.1
|lat| 30–45° mean 255.5 min 127.8 max 305.1
|lat| 45–60° mean 247.0 min 127.8 max 305.1
|lat| 60–75° mean 251.4 min 127.8 max 305.1
|lat| 75–90° mean 265.4 min 204.8 max 305.1
It took 3.5 s for two million cells. The smallest and largest values recur in almost every band, because each icosahedron face spans many latitudes and repeats the same distortion pattern. Resolution 6 has seven times as many cells, so profile a coarser level and scale the areas down by powers of seven.
Example 3 — refuse coordinates H3 would silently accept
import numpy as np
def check_latlng(df, lat="lat", lon="lon"):
"""Refuse coordinates H3 would silently accept and place somewhere else."""
la, lo = df[lat].to_numpy(), df[lon].to_numpy()
bad_lat = np.abs(la) > 90
bad_lon = np.abs(lo) > 180
report = {
"rows": len(df),
"lat_out_of_range": int(bad_lat.sum()),
"lon_out_of_range": int(bad_lon.sum()),
"non_finite": int((~np.isfinite(la) | ~np.isfinite(lo)).sum()),
}
if bad_lat.any() and not (np.abs(lo) > 90).any():
report["hint"] = "every longitude fits in ±90 and some latitudes do not: columns swapped?"
return report
On GeoNames as downloaded, and with the two columns swapped:
{'rows': 13464117, 'lat_out_of_range': 0, 'lon_out_of_range': 0, 'non_finite': 0}
{'rows': 13464117, 'lat_out_of_range': 4654000, 'lon_out_of_range': 0, 'non_finite': 0, 'hint': 'every longitude fits in ±90 and some latitudes do not: columns swapped?'}
The check catches a global swap because a third of the world's longitudes are beyond ±90. It cannot catch a swap in a dataset confined to, say, Europe, where both columns fit either range. There, compare the result with a known bounding box.
Explanation
Why an icosahedron and a gnomonic projection
Any flat grid laid on a sphere has to be distorted somewhere. The more faces the base polyhedron has, the less of the sphere each face covers, and the less the projection has to stretch. The icosahedron has the most faces of any regular solid.
The gnomonic projection maps great circles to straight lines. That keeps each face's triangular lattice tidy and makes cell edges great-circle arcs. The distortion grows towards a face's corners, so cell size depends on position within a face rather than on latitude.
Why there must be exactly 12 pentagons
A closed surface tiled only by hexagons and pentagons, three meeting at each vertex, needs exactly 12 pentagons. That follows from Euler's formula; a football is the familiar example. H3 cannot avoid them. It can only choose where they go, and it put them at the icosahedron's vertices, all over water. Their fixed position is why one list from h3.get_pentagons(res) covers every resolution.
Why aperture 7 and the rotation
Aperture 7 gives every hexagon exactly seven children: one centre child and six around it. Seven values fit in a 3-bit digit (0 to 6), and the eighth value, 7, is left over to mark unused digits. The cost is geometric: the children's lattice is rotated about 19.1°, their union is a jagged outline instead of the parent hexagon, and about 7.14% of the children's area lies outside the parent. The index hierarchy stays exact, since the digits are a clean prefix, while the geometry only approximates it. The hierarchy guide measures what that does to aggregations.
Why the index is an integer
Sixty-four bits per cell fits in a uint64 column, sorts cheaply, and compresses well. Two million keys took 16.0 MB as integers against 46.0 MB as Python strings. Sorting by the integer also keeps cells of the same base cell and coarse digits together, so range scans over a region stay local.
The hexadecimal string is the same number printed in base 16. Converting between the two with str_to_int and int_to_str is lossless.
Edge cases or notes
- Invalid latitudes wrap instead of failing. A latitude of 95 indexes to a cell near 85° on the opposite meridian. Validate coordinate ranges first.
- Pentagons have five neighbours and six children.
grid_ringaround one returns 5 cells, and somegrid_distancecalls near them raiseH3FailedError. - Odd resolutions have extra vertices. 0.73% of resolution-5 cells draw with 7–10 vertices, so never assume six.
cell_areauses a sphere. It agreed with WGS84 geodesic areas to within 0.9% on 2,000 cells, which is close enough for density maps.- Web Mercator areas are badly wrong. Hexagon areas in EPSG:3857 were 1.50× the true value at the median for occupied cells, and up to 56.6×.
- The index already contains its resolution, so cells from mixed resolutions can share a column. They still never compare equal.
- The v4 API renamed nearly everything.
geo_to_h3becamelatlng_to_cell, and v3 code fails with anAttributeError. - Cells crossing the antimeridian draw as bands across the map unless their longitudes are shifted: 1,547 at resolution 5.
Internal links
- Discrete global grids explained: H3, S2 and geohash compared — where H3 sits among the alternatives
- H3 hierarchy explained: parents, children and why they do not nest — the consequence of aperture 7
- Choosing an H3 resolution — cell sizes and counts at all 16 levels
- How to assign points to H3 cells in Python — indexing a real dataset quickly
- How to turn H3 cells into a GeoDataFrame — polygons with any vertex count
- Fixing H3 cells in the wrong place — the latitude and longitude order trap
- Fixing H3 hexagons that stretch across the map — the antimeridian cells
- Fixing H3 AttributeError after upgrading — the v3 to v4 rename
- How to use H3 neighbours for k-ring smoothing — grid_disk and pentagons in practice
FAQ
What is H3?
A hierarchical grid of hexagonal cells covering the whole Earth at 16 resolutions, originally developed at Uber. Each cell has a 64-bit index that encodes its base cell, its resolution and its position within each coarser cell.
Are all H3 cells hexagons?
No. Every resolution has exactly 12 pentagons, placed at the vertices of the underlying icosahedron, all of them in the ocean. Only 31 of 13.46 million GeoNames points fell in a resolution-5 pentagon.
Are H3 cells the same size?
No. At resolution 5 hexagons range from 153.8 to 305.1 km², a 1.98× spread. The spread follows each cell's position on an icosahedron face, not its latitude.
What do the characters in an H3 index mean?
The string is a 64-bit integer in hexadecimal. It holds a mode, the resolution, a base cell from 0 to 121, and one 3-bit digit per resolution step. Unused digits are 7, which is why coarse cells end in runs of f.
Why did H3 accept a latitude of 95?
latlng_to_cell wraps out-of-range values instead of rejecting them, so 95° N lands near 85° N on the opposite meridian. Only NaN and infinite values raise H3LatLngDomainError, so range-check coordinates before indexing.
Is H3 the same as a hexbin?
No. A hexbin is laid out on a flat projection for one plot and has no identifiers that persist between datasets. H3 cells are fixed on the globe, so two datasets indexed separately still share keys.