Fundamentals · 8 min read · October 2026

H3 hexagons, explained.

Uber's hierarchical hexagonal index is the standard way to bucket spatial events for analytics. Here's how the system is actually built, and what the IDs mean when you look at them.

The problem it solves

Imagine you run a ride-share operation and want to know pickup density across a city, broken down by half-hour. You need to count events per spatial bucket. What are the buckets?

The naive answer is "a grid of latitude/longitude squares". That works, briefly, until you notice the problems: squares a degree wide at the equator shrink to narrow slivers near the poles; neighbouring squares are closer than diagonal squares, so any analysis sensitive to distance is skewed; and the whole tessellation is tied to an arbitrary coordinate system that doesn't carve any real territory sensibly.

Administrative boundaries — zip codes, census tracts, districts — avoid the geometric problems but introduce new ones: they're wildly uneven in area, they change over time, and they encode political choices that make cross-city comparison awkward.

A hexagonal grid, applied uniformly across the Earth and parameterised by resolution, side-steps all of this. H3 is Uber's open-source implementation of such a grid.

How the Earth gets hexagons

You can't actually tile a sphere with hexagons alone — a classical result from topology. The nearest you can get is twelve pentagons, with the rest hexagons. H3 does exactly that: at every resolution, there are exactly twelve pentagon cells (artifacts of how the base icosahedron projects onto a sphere), and the rest of the surface is hexagons.

The construction starts with an icosahedron — a 20-sided polyhedron whose faces are triangles. The twelve pentagons live at the icosahedron's vertices. H3 then subdivides those triangular faces into a hex grid, and projects the result onto the sphere using a Dymaxion-like projection. The pentagon locations were chosen deliberately: they mostly sit over the oceans, so day-to-day analytics on populated land can usually ignore them.

The base layer — resolution 0 — has 122 cells: 110 hexagons and 12 pentagons. Each subsequent resolution subdivides every cell into roughly seven children, giving:

ResolutionCell countAvg. area
01224.3 million km²
1842609 000 km²
52.0 million253 km²
8691 million0.74 km²
1033.9 billion15 100 m²
15569 trillion0.9 m²

Aperture 7: the tricky bit

If you tile a plane with regular hexagons, you can't perfectly subdivide one into smaller hexagons the way you can divide a square into four smaller squares. The nearest hex-only subdivision rotates the child cells by ~19.1° relative to the parent and gives you a central child plus six corners — seven children, but with small overlaps and gaps at the edges.

H3 calls this an aperture-7 subdivision. The hierarchy is approximate rather than exact: a resolution-9 cell is mostly contained within one resolution-8 cell, but boundary points on the edges of that resolution-8 cell may actually belong to a neighbouring parent. The approximation gets slightly worse as you traverse multiple levels.

For most analytics this doesn't matter. If you're joining ride pickups to zones, counting events per hex, or rolling up a dashboard from fine to coarse resolution, the edge errors are far below any signal you care about. Where it does matter — simulation, precise geometric reasoning, radar sweeps — you reach for a different index.

Reading an H3 cell ID

H3 cell IDs are 64-bit integers, usually written as 15-character hexadecimal strings. Here's one from Dubai:

882a100d61fffff

That string encodes:

You almost never need to parse this by hand — h3-js and its sibling libraries have accessors for every field — but it's useful to know the structure exists. The all-7 padding is why child cells share a common prefix with their parent: dropping digits from the right walks you up the hierarchy without having to look anything up.

The core operations

Three operations do most of the work in day-to-day H3 use:

Point-to-cell

h3.latLngToCell(lat, lng, resolution). Takes a point, returns the cell containing it at the chosen resolution. This is your indexing step — the one you run on every event before storing it. It's cheap: no spatial index to maintain, no polygon queries, just a few bit shifts after projection.

Parent / children

h3.cellToParent(cell, resolution) and h3.cellToChildren(cell, resolution). Walk the hierarchy. Parent is O(1); children returns the ~7k cells contained in the given cell at the finer target resolution. (This is what H3Split does, in bulk, for arbitrarily many parents.)

k-ring / grid-disk

h3.gridDisk(cell, k). All cells within k steps of the given one. The hex-grid version of "nearby points" — used for radius queries, local smoothing, neighbour lookups.

What you do once you have cells

Index your data to H3 at a reasonable raw resolution — 11 or 12 is typical for events — and store the cell ID alongside the lat/lng. From there, pretty much every analytical move is a group-by on cell. If you need coarser aggregation, derive the parent at index time (parent at 10, parent at 9, parent at 8) and store those as additional columns. Now you can roll up without recomputing.

For zone definition — service areas, incentive zones, no-go areas — maintain the zones as a set of H3 cells rather than polygons. Joins become set membership checks. Changes become set operations.

Further reading

← Back to the tool