Planetary-scale answers, unlocked.
A Hands-On Guide for Working with Large-Scale Spatial Data. Learn more.
Authors
H3 is an open-source geospatial indexing system that divides the Earth into hexagonal cells and gives each cell a 64-bit ID. The uber/h3 repository describes it as a "hexagonal hierarchical geospatial indexing system". Uber built H3 to price rides and dispatch drivers by area. Isaac Brodsky introduced it on Uber's engineering blog in June 2018. That year Uber open sourced the library under the Apache 2.0 license. The grid has 16 resolutions, from cells larger than four million square kilometers to cells under one square meter. Converting a latitude and longitude to an H3 cell turns a location into a number that databases can group, sort, and join. H3 appears across mobility, insurance, retail, and geospatial analysis. This article tests the claim that hexagons give each cell neighbors at one distance and nearly equal area, but a cell's seven children only approximately fill it. So H3 works best to narrow candidate matches and to count records per area. Records near cell edges need an exact geometry check.
An icosahedron is a solid with 20 triangular faces and 12 vertices. The H3 overview describes the construction. The hexagonal grid is laid on the flat faces of an icosahedron that encloses the sphere. A gnomonic projection, a projection through the center of the sphere, then maps each cell onto the sphere. That sphere has the authalic radius of the World Geodetic System 1984 (WGS 84) ellipsoid: the radius of a sphere with the same surface area as the Earth's ellipsoid. The icosahedron is fixed to the Earth in R. Buckminster Fuller's Dymaxion orientation, which places all 12 vertices in the ocean.
A sphere can't be tiled with hexagons alone, so each H3 resolution holds 12 pentagons, one centered on each icosahedron vertex. Putting the vertices at sea keeps the pentagons, and the distortion around them, away from most land data. Resolution 0 has 122 base cells, 110 hexagons and 12 pentagons, numbered 0 to 121 from north to south. Every finer cell descends from one base cell, and the cell count table gives the number of cells at resolution r as 2 + 120 × 7^r.
Each finer resolution uses aperture 7. Every hexagon has seven children, a center child and six around it. Each child has one seventh of its parent's area on the icosahedron. A pentagon has six children, five hexagons and one pentagon. Seven hexagons can't tile a larger hexagon, so the H3 coordinate system rotates each resolution about 19.1° from the one before it. Resolutions alternate between two orientations, which H3 calls Class II and Class III after Fuller's terms.
The H3 indexing documentation calls the result approximate containment. In the index, every cell has one parent, and a few bitwise operations on the child's ID give the parent's ID. Geographic containment is approximate: part of each child lies outside its parent's boundary. A point indexed at resolution 9 and truncated to resolution 8 can land in a different cell than the same point indexed directly at resolution 8. Cell boundaries at a single resolution are exact, so neighbor lookups and point indexing at one resolution are unaffected.
An H3 cell ID packs its path through the hierarchy into a 64-bit integer. The H3 cell index layout assigns the bits from the highest down:
Bit shifts in WherobotsDB decode the right-hand column:
SELECT lower(hex(h)) AS h3_hex, shiftright(h, 59) & 15 AS mode, shiftright(h, 52) & 15 AS resolution, shiftright(h, 45) & 127 AS base_cell, concat_ws('', transform(sequence(1, 15), i -> CAST(shiftright(h, (15 - i) * 3) & 7 AS STRING))) AS digits_1_to_15 FROM (SELECT CAST(617700169994207231 AS BIGINT) AS h) t -- 89283082a33ffff | 1 | 9 | 20 | 060405214777777
Each digit from 0 to 6 picks one of the seven children at that resolution, and unused digits are set to 7. The digits follow the central place indexing arrangement that Kevin Sahr described, published in full in Central place indexing (Cartographica, 2019). The canonical string form is the lowercase hexadecimal of the integer, so 617700169994207231 is written 89283082a33ffff. Store the ID as a 64-bit integer or a string. Casting it through a floating-point type rounds away the last digits and points to the wrong cell. Cells that share a parent share the leading bits of their IDs, so sorting on the integer keeps siblings together on disk.
Values come from the H3 resolution table, computed on a sphere with the WGS 84 authalic radius.
Area varies with a cell's position relative to the icosahedron's vertices. At resolution 9 the largest hexagon is 1.99 times the area of the smallest, and pentagons are about half the area of an average hexagon.
Three regular polygons tile the plane: triangles, squares, and hexagons. In the H3 aggregation notes, a triangle has 12 neighbors at three distances. A square has eight neighbors at two distances: four that share an edge and four that share a corner. A hexagon has six neighbors, all sharing an edge, with every neighbor center at the same distance. On a hexagonal grid, smoothing and movement behave alike in every direction, because grid distance tracks geographic distance.
Hexagons are the most compact of the three shapes. Thomas Hales proved the honeycomb conjecture in 2001: of all partitions of the plane into equal-area regions, the regular hexagonal tiling has the least perimeter. Birch, Oom, and Beecham compared rectangular and hexagonal grids in ecology (2007).
The H3 function gridDisk, also called a k-ring, returns every cell within k steps of a cell. For a hexagon away from a pentagon, that is 1 + 3k(k + 1) cells: 7 cells for k = 1 and 19 for k = 2. The hollow ring of cells k steps away holds 6k cells. Grid distance is the minimum number of steps between two cells, and the H3 traversal functions note that it can fail across pentagon distortion.
S2 projects the sphere onto the six faces of a cube. It orders cells on each face along a Hilbert curve, a path that visits every cell once. That order gives every cell from level 0 to 30 a 64-bit ID. In the H3 comparison with S2, S2 squares split into four children with no overlap, and H3 hexagons give one neighbor type at the cost of approximate containment. Geohash interleaves the bits of longitude and latitude, the Z-order idea G. M. Morton described in a 1966 IBM report, and writes the result in base 32. The H3 comparison with geohash notes that geohash cells shrink toward the poles because a degree of longitude spans less distance at high latitude. Quadkeys name the tiles of the Bing Maps tile system, a pyramid of square tiles in the Web Mercator projection. Its tiles cover less and less ground the farther they are from the equator. They stop at 85.05° north and south.
Area distortion separates them most. This WherobotsDB query returns the area of the cell that contains a point every 5° of latitude along longitude 122.42° W, the meridian through San Francisco:
WITH pts AS ( SELECT CAST(lat AS DOUBLE) AS lat, ST_Point(-122.42, CAST(lat AS DOUBLE)) AS pt FROM (SELECT explode(sequence(0, 80, 5)) AS lat) s ) SELECT lat, ROUND(ST_AreaSpheroid(ST_H3ToGeom(ST_H3CellIDs(pt, 7, false))[0]) / 1e6, 3) AS h3_res7_km2, ROUND(ST_AreaSpheroid(ST_GeomFromGeoHash(ST_GeoHash(pt, 5))) / 1e6, 3) AS geohash5_km2, ROUND(ST_AreaSpheroid(ST_BingTilePolygon(ST_BingTileAt(-122.42, lat, 14))) / 1e6, 3) AS quadkey14_km2 FROM pts ORDER BY lat
From the equator to 80° N, the five-character geohash cell shrinks from 23.77 km² to 4.18 km². Over the same range, the zoom 14 quadkey tile shrinks from 5.94 km² to 0.18 km², 3% of its equatorial area. The H3 resolution 7 cell stays between 4.26 km² and 5.76 km² along the whole line. H3's variation follows position on the icosahedron, so it rises and falls along the line with no trend toward the pole.
WherobotsDB and Apache Sedona run H3 in SQL:
ST_H3CellIDs(geom, level, fullCover)
ST_H3ToGeom(cells)
ST_H3KRing(cell, k, exactRing)
exactRing
ST_H3CellDistance(cell1, cell2)
ST_H3ToParent(cell, resolution)
The same dialect has functions for S2 cells, geohashes, and Bing tile quadkeys, which the grid comparison above uses. S2 is left out of the chart because ST_S2ToGeom returns each cell's latitude and longitude bounding box, which overstates its area.
The running example is Overture places in a box around San Francisco, from the Havasu catalog. The queries in this article run as WherobotsDB SQL and read these inputs:
wherobots_open_data.overture_maps_foundation.places_place
sequence
This query indexes each place at resolution 9 and counts places per cell:
SELECT CAST(h3 AS STRING) AS h3_cell, lower(hex(h3)) AS h3_index, COUNT(*) AS places FROM ( SELECT ST_H3CellIDs(geometry, 9, false)[0] AS h3 FROM wherobots_open_data.overture_maps_foundation.places_place WHERE ST_Intersects(geometry, ST_PolygonFromEnvelope(-122.53, 37.70, -122.35, 37.82)) ) GROUP BY h3 ORDER BY places DESC LIMIT 5
A second query explores the busiest cell:
SELECT lower(hex(ST_H3ToParent(617700169994207231, 7))) AS parent_res7, size(ST_H3KRing(617700169994207231, 1, false)) AS ring_1_cells, size(ST_H3KRing(617700169994207231, 2, false)) AS ring_2_cells, size(ST_H3KRing(617700169994207231, 2, true)) AS ring_2_exact, ST_H3CellDistance(617700169994207231, 617700169993158655) AS grid_distance, ROUND(ST_AreaSpheroid(ST_H3ToGeom(array(617700169994207231))[0])) AS area_sq_m
Its resolution 7 parent is 87283082affffff. One ring holds 7 cells, two rings hold 19, and the ring of cells two steps away holds 12. The second-busiest cell is 1 step away. The hexagon covers 109,461 m² on the WGS 84 ellipsoid, 3.9% above the 105,333 m² spherical average for resolution 9.
The next query indexes every place at resolutions 7, 8, and 9. It then counts how often the truncated parent of a resolution 9 cell differs from the cell of the same place at the coarser resolution. It ran in 37 seconds.
WITH p AS ( SELECT ST_H3CellIDs(geometry, 9, false)[0] AS h9, ST_H3CellIDs(geometry, 8, false)[0] AS h8, ST_H3CellIDs(geometry, 7, false)[0] AS h7 FROM wherobots_open_data.overture_maps_foundation.places_place WHERE ST_Intersects(geometry, ST_PolygonFromEnvelope(-122.53, 37.70, -122.35, 37.82)) ) SELECT COUNT(*) AS places, COUNT(DISTINCT h9) AS res9_cells, COUNT(DISTINCT h8) AS res8_cells, COUNT(DISTINCT h7) AS res7_cells, SUM(CASE WHEN ST_H3ToParent(h9, 8) <> h8 THEN 1 ELSE 0 END) AS parent8_mismatch, SUM(CASE WHEN ST_H3ToParent(h9, 7) <> h7 THEN 1 ELSE 0 END) AS parent7_mismatch FROM p
The box holds 74,373 places in 1,164 resolution 9 cells, 201 resolution 8 cells, and 42 resolution 7 cells. For 4,455 places (6.0%), the resolution 8 parent of their resolution 9 cell is a different cell from the one they fall in at resolution 8. Truncating two levels to resolution 7 moves 4,761 places (6.4%).
A single scan returns the cells at both resolutions. Expanding a two-entry map into rows gives each place one row per resolution, so the table is read once:
WITH p AS ( SELECT ST_H3CellIDs(geometry, 9, false)[0] AS h9, ST_H3CellIDs(geometry, 8, false)[0] AS h8 FROM wherobots_open_data.overture_maps_foundation.places_place WHERE ST_Intersects(geometry, ST_PolygonFromEnvelope(-122.53, 37.70, -122.35, 37.82)) ), cells AS ( SELECT res, cell, COUNT(*) AS places FROM p LATERAL VIEW explode(map(9, h9, 8, h8)) t AS res, cell GROUP BY res, cell ) SELECT res, COUNT(*) AS cells, SUM(places) AS places, MAX(places) AS max_places, percentile(places, 0.5) AS median_places, SUM(CASE WHEN places = 1 THEN 1 ELSE 0 END) AS single_place_cells, ROUND(MIN(ST_AreaSpheroid(ST_H3ToGeom(array(cell))[0]))) AS min_area_m2, ROUND(MAX(ST_AreaSpheroid(ST_H3ToGeom(array(cell))[0]))) AS max_area_m2 FROM cells GROUP BY res ORDER BY res
The query ran in under 5 seconds. One step finer, the busiest cell holds a quarter as many places, the median cell an eighth as many, and 99 cells hold a single place. Across the city, cell areas at one resolution differ by less than 0.3%, so local counts compare fairly.
The reassignments show in the busiest resolution 8 cell, 88283082a3fffff, downtown. This query compares the places indexed directly to it with the places in its seven children:
WITH p AS ( SELECT ST_H3CellIDs(geometry, 9, false)[0] AS h9, ST_H3CellIDs(geometry, 8, false)[0] AS h8 FROM wherobots_open_data.overture_maps_foundation.places_place WHERE ST_Intersects(geometry, ST_PolygonFromEnvelope(-122.53, 37.70, -122.35, 37.82)) ), flagged AS ( SELECT h9, lower(hex(h8)) = '88283082a3fffff' AS in_res8_cell, lower(hex(ST_H3ToParent(h9, 8))) = '88283082a3fffff' AS parent_is_cell FROM p ) SELECT SUM(CASE WHEN in_res8_cell THEN 1 ELSE 0 END) AS direct_res8_count, SUM(CASE WHEN parent_is_cell THEN 1 ELSE 0 END) AS children_rollup_count, SUM(CASE WHEN parent_is_cell AND NOT in_res8_cell THEN 1 ELSE 0 END) AS in_child_outside_parent, SUM(CASE WHEN in_res8_cell AND NOT parent_is_cell THEN 1 ELSE 0 END) AS in_parent_not_in_child, COUNT(DISTINCT CASE WHEN in_res8_cell AND NOT parent_is_cell THEN h9 END) AS foreign_res9_cells FROM flagged WHERE in_res8_cell OR parent_is_cell
Indexed directly, the cell holds 11,745 places. Its seven children hold 11,829. The difference comes from both edges. 326 places sit in a child but outside the parent. Another 242 sit inside the parent but in 6 resolution 9 cells that belong to neighboring parents.
For counts and maps, a 6% reassignment at the edges may be acceptable. For totals that must match where each record falls, such as exposure totals, index each record directly at every resolution the analysis reports.
Every resolution 3 cell fits in one query: a lattice of points every half degree hits each cell, and ST_AreaSpheroid gives its area on the WGS 84 ellipsoid. A BROADCAST hint copies the short latitude list to every worker for the cross join. The query finished in 11 seconds.
BROADCAST
WITH lon AS (SELECT v / 100.0 AS lon FROM (SELECT explode(sequence(-17975, 17975, 50)) AS v) a), lat AS (SELECT v / 100.0 AS lat FROM (SELECT explode(sequence(-8975, 8975, 50)) AS v) b), lattice AS (SELECT /*+ BROADCAST(lat) */ ST_Point(lon, lat) AS pt FROM lon CROSS JOIN lat), cells AS (SELECT DISTINCT ST_H3CellIDs(pt, 3, false)[0] AS cell FROM lattice), areas AS (SELECT cell, ST_H3ToGeom(array(cell))[0] AS g FROM cells), measured AS ( SELECT cell, ST_NPoints(g) AS npts, ST_AreaSpheroid(g) / 1e6 AS km2, ROW_NUMBER() OVER (ORDER BY ST_AreaSpheroid(g)) AS rk FROM areas WHERE ST_XMax(g) - ST_XMin(g) < 180 -- drop cells split by the antimeridian or around a pole ) SELECT (SELECT COUNT(*) FROM cells) AS cells_found, COUNT(*) AS cells_measured, ROUND(AVG(CASE WHEN rk <= 12 THEN km2 END), 1) AS smallest12_avg_km2, ROUND(MIN(CASE WHEN rk > 12 THEN km2 END), 1) AS min_hexagon_km2, ROUND(MAX(km2), 1) AS max_hexagon_km2, ROUND(AVG(CASE WHEN rk > 12 THEN km2 END), 1) AS avg_hexagon_km2, ROUND(MAX(km2) / MIN(CASE WHEN rk > 12 THEN km2 END), 3) AS max_min_ratio FROM measured
The lattice yields 41,162 cells, the exact count in the H3 resolution table. After dropping 218 cells that cross the antimeridian or surround a pole, the 12 smallest of the remaining 40,944 average 6,315.5 km², the documented pentagon area. The hexagons range from 7,691.1 km² to 15,071.5 km², a ratio of 1.96, and average 12,395.4 km². The resolution table's spherical figures are 1.94 and 12,393 km², close to the ellipsoidal ones.
The Apache Iceberg spatial data post covers H3 as a sort and partition key, and the mobility solution accelerator indexes GPS points at resolution 9 for density heatmaps.
H3 is one design in a field that predates it by decades: discrete global grid systems (DGGS).
Kimerling, Sahr, White, and Song compared tessellations of the sphere in Comparing geometrical properties of global grids (1999), measuring how cell area, shape, and spacing vary across candidate designs. Sahr, White, and Kimerling then framed the design space in Geodesic discrete global grid systems (Cartography and Geographic Information Science, 2003), the paper the H3 documentation cites. They showed that a geodesic DGGS comes from five largely independent choices. The first two are a base polyhedron and its orientation to the Earth. The other three are a hierarchical partition of the faces, a transform from the faces to the sphere, and a way to assign points to cells. H3's choices are an icosahedron, the Dymaxion orientation, aperture 7 hexagons, a gnomonic projection, and cell centers.
The projection choice sets the area behavior. John Snyder's equal-area map projection for polyhedral globes (1992) gives every cell at a resolution the same area, and Sahr's DGGRID software builds equal-area icosahedral grids on it. H3's gnomonic projection keeps cell shapes closer to regular hexagons and accepts the twofold area range in exchange. Astronomy's HEALPix grid (Górski and colleagues, 2005) is equal-area too.
Richard Barnes revisited the orientation choice in Optimal orientations of discrete global grids and the Poles of Inaccessibility (2019). He found other orientations that also keep all 12 vertices at sea.
The Open Geospatial Consortium (OGC) standardized the concept in Topic 21, Discrete Global Grid Systems, Part 1 (OGC 20-040r3, published 2021). The International Organization for Standardization (ISO) published it in parallel as ISO 19170-1:2021. The specification defines a DGGS as a system of cell IDs tied to cell geometry, plus functions to import data, export data, and query which cells touch. It also requires cells of equal area. H3 does not meet that requirement, because its cell areas vary by a factor of about two. The OGC API, Discrete Global Grid Systems, Part 1: Core followed as a web API standard for retrieving data by zone.
Carr, Littlefield, Nicholson, and Littlefield introduced hexagon binning for dense scatterplots in Scatterplot matrix techniques for large N (Journal of the American Statistical Association, 1987). In the H3 comparison with hexbins, a hexbin grid belongs to one map projection and has no hierarchy, while H3 cell IDs are global and portable between datasets.
Any grid is a choice of areal unit, so H3 inherits the modifiable areal unit problem. Stan Openshaw's 1984 monograph, The Modifiable Areal Unit Problem (CATMOG 38), showed that statistics change with both the size and the placement of the zones. Jelinski and Wu traced the implications for landscape ecology (1996). The San Francisco counts above show the size effect: one resolution finer, the busiest cell drops from 11,745 places to 2,771.
At billions of rows, the cell ID becomes a partitioning key. Datasets indexed to one resolution join on equal 64-bit cell IDs, which a cluster hashes and distributes without reading geometry. Jia Yu, Jinxuan Wu, and Mohamed Sarwat built spatial partitioning and indexing into Apache Spark with GeoSpark (ACM SIGSPATIAL, 2015). Yu, Zhang, and Sarwat wrote Spatial data management in Apache Spark: the GeoSpark perspective and beyond (GeoInformatica, 2019). It compared four ways to split data into partitions by location: uniform grid, R-tree, quadtree, and KDB-tree. GeoSpark became Apache Sedona, the engine behind the ST_H3 functions in this article.
These queries show what H3 is: near-equal hexagons with one neighbor type and resolutions that nest only approximately.
ST_Intersects
fullCover = true
ST_H3ToParent
Index and aggregate billions of points to H3 cells with a Wherobots free trial at cloud.wherobots.com.
H3 is an open-source geospatial indexing system created at Uber that divides the Earth into hexagonal cells at 16 resolutions. Each cell has a 64-bit ID, so any location can be converted to a cell and datasets can be grouped or joined on the ID. Uber open sourced H3 in 2018 under the Apache 2.0 license.
In geospatial work, H3 is the name of Uber’s hexagonal hierarchical geospatial indexing system. It divides the Earth into hexagonal cells arranged in a hierarchy and gives each cell a 64-bit ID. The hierarchy runs from 122 base cells at resolution 0 down to cells under one square meter at resolution 15.
H3 projects the Earth onto the 20 faces of an icosahedron and lays a hexagonal grid on those faces. The icosahedron is fixed in the Dymaxion orientation, so its 12 vertices sit in the ocean. Resolution 0 has 122 cells. Each finer resolution splits every hexagon into seven smaller ones, an arrangement called aperture 7. A point is indexed by finding the cell that contains it at the chosen resolution.
The index level, or resolution, sets the cell size. H3 has 16 resolutions, numbered 0 to 15. Resolution 0 hexagons average about 4.36 million square kilometers, resolution 9 hexagons about 0.105 square kilometers with edges of about 201 meters, and resolution 15 hexagons under one square meter.
Polyfill returns the set of H3 cells that cover a polygon at a given resolution. In H3 version 4 the function is called polygonToCells. In Wherobots and Apache Sedona, ST_H3CellIDs runs polyfill on polygons, and its fullCover option adds cells so the polygon is completely covered.
Every neighbor of a hexagon shares an edge with it, and all neighbor centers sit at the same distance. Square grids have two kinds of neighbors, edge and corner, at different distances. A single neighbor type makes smoothing, movement, and distance calculations on the grid simpler.
No. Every resolution has 12 pentagons, centered on the vertices of the icosahedron. All other cells are hexagons. A pentagon has five neighbors and six children, and its area is about half that of an average hexagon at the same resolution.
Both are open-source, hierarchical, global grids with 64-bit cell IDs. S2 uses square cells on the six faces of a cube. Each S2 cell splits into four children that fill it with no gap or overlap. H3 uses hexagonal cells on an icosahedron. Each H3 cell splits into seven children that only approximately fill it. In exchange, every H3 neighbor shares an edge and sits at the same distance.
Geohash interleaves the bits of longitude and latitude and writes them as a base-32 string, so its cells are rectangles in degrees that shrink toward the poles. H3 cells are hexagons on an icosahedron with 64-bit integer IDs, and their area varies by about a factor of two worldwide.
Match the resolution to the precision of the input and to the decision. GPS points accurate to about 10 meters cannot support resolution 13, and the city examples in this article use resolutions 7 to 9. Count how many records land in each cell at two candidate resolutions before choosing.
A k-ring, or grid disk, is the set of cells within k steps of a cell. For a hexagon it holds 1 + 3k(k + 1) cells: 7 for k = 1 and 19 for k = 2. The hollow ring of cells k steps away holds 6k cells. In Wherobots, ST_H3KRing returns either set.
No. Seven hexagons can’t tile a larger hexagon, so H3 rotates each resolution about 19.1 degrees. The children cover their parent only approximately. In the index, each child still has one parent ID. In a Wherobots query on 74,373 San Francisco places, 6.0% fell in a different resolution 8 cell than the parent of their resolution 9 cell.
h3-py is the Python binding for the H3 C library. It exposes the core operations, such as converting a point to a cell, getting a cell’s boundary, and finding neighbors. For datasets too large for one machine, the same operations run in SQL with the ST_H3 functions in Wherobots and Apache Sedona.
What is a Geofence? How Geofencing Works, With Examples
A geofence is a virtual boundary that triggers an action when a device enters or leaves it. How geofencing works, its uses and limits, and a test on ship data.
What is Map Matching? GPS Traces, Methods and Examples
Map matching places each GPS record on the road a vehicle drove. How it works, nearest-road snapping vs hidden Markov models, and a test on Ann Arbor drives.
Geospatial Big Data: Why Analytics Is Hard to Scale
Teams struggle to scale geospatial analytics for five reasons: data volume, mixed formats, many coordinate systems, spatial joins and pipeline upkeep.
share this article
Awesome that you’d like to share our articles. Where would you like to share it to: