h3-pg PostgreSQL Extension
Extension Architecture
Two extensions:
CREATE EXTENSION h3 — Core H3 bindings (indexing, traversal, hierarchy, inspection, <-> operator). No PostGIS dependency.
CREATE EXTENSION h3_postgis — PostGIS integration (geometry/geography casts, @ operator, polygon fill, raster helpers). Requires postgis and optionally postgis_raster.
Postgres-Specific H3 Features
These exist only in h3-pg, not other H3 language bindings.
Direct casts (h3_postgis):
h3index::geometry → cell centroid as POINT, SRID 4326
h3index::geography → cell centroid as POINT, SRID 4326
Operators:
a <-> b — grid distance in cells between two h3index values (h3)
geom @ resolution — index geometry/geography at resolution (h3_postgis)
Inspection and construction helpers (h3):
h3_get_index_digit(cell, resolution) — inspect the digit at a 1-based resolution step
h3_construct_cell(resolution, base_cell_number, digits[]) — rebuild a cell from explicit components
h3_is_valid_index(index) — validate any H3 index mode: cell, directed edge, or vertex
Directed edge convenience (h3):
h3_reverse_directed_edge(edge) — returns the same directed edge with origin and destination reversed
Operator classes for h3index: BTREE, HASH, BRIN, SP-GIST (h3)
Boundary helpers (h3_postgis):
h3_cell_to_boundary_geometry(h3index) → polygon with SRID 4326, splits at antimeridian
h3_cell_to_boundary_geography(h3index) → geography polygon, splits at antimeridian
Polygon fill (h3_postgis):
h3_polygon_to_cells(geometry, resolution) → SETOF h3index
h3_polygon_to_cells(geography, resolution) → SETOF h3index
Raster integration (h3_postgis, requires postgis_raster):
h3_raster_summary(raster, resolution, [band]) — per-cell stats (count, sum, mean, stddev, min, max)
h3_raster_summary_clip(...) — clips raster by cell boundaries
h3_raster_summary_centroids(...) — uses cell centroids, avoids pentagon edge cases
h3_raster_summary_subpixel(...) — for cells smaller than pixels
h3_raster_class_summary*(...) — discrete class counts per cell
Tile helper (h3_postgis):
h3_get_resolution_from_tile_zoom(z, [max_h3_resolution=15], [min_h3_resolution], [hex_edge_pixels=44], [tile_size=512]) — returns optimal H3 resolution for XYZ tile zoom level z, targeting hexagons approximately hex_edge_pixels wide on screen
Common Pattern Fixes
| Pattern |
DO NOT |
DO |
| Get centroid |
ST_Centroid(h3_cell_to_boundary_geometry(h3)) |
h3::geometry |
| Get boundary with SRID |
h3_cell_to_boundary(h3)::geometry |
h3_cell_to_boundary_geometry(h3) |
| Distance check (meters) |
ST_Distance(a.h3::geography, b.h3::geography) <= N |
ST_DWithin(a.h3::geography, b.h3::geography, N) |
| Grid distance (cells) |
h3_distance(a, b) |
a <-> b |
Antimeridian gotcha: Cells crossing 180° are split into valid polygons. ST_Centroid of a split polygon may fall outside the cell — use h3::geometry for centroids instead.
Ring traversal gotcha: Prefer h3_grid_ring(origin, k) for rings because it handles pentagon distortion internally. Use h3_grid_ring_unsafe(origin, k) only when fail-fast behavior or ring-walk ordering matters.
Critical v3 → v4 Renames
| v3 Name (DO NOT USE) |
v4 Replacement |
h3_geo_to_h3 |
h3_latlng_to_cell |
h3_to_geo |
h3_cell_to_latlng |
h3_to_geo_boundary |
h3_cell_to_boundary |
h3_k_ring |
h3_grid_disk |
h3_k_ring_distances |
h3_grid_disk_distances |
h3_hex_ring |
h3_grid_ring_unsafe |
h3_line |
h3_grid_path_cells |
h3_distance |
h3_grid_distance |
h3_to_parent |
h3_cell_to_parent |
h3_to_children |
h3_cell_to_children |
h3_compact |
h3_compact_cells |
h3_uncompact |
h3_uncompact_cells |
h3_polyfill |
h3_polygon_to_cells |
h3_indexes_are_neighbors |
h3_are_neighbor_cells |
h3_hex_area |
h3_get_hexagon_area_avg |
h3_hex_area_km2 / h3_hex_area_m2 |
h3_get_hexagon_area_avg(res, unit) |
h3_edge_length_km / h3_edge_length_m |
h3_get_hexagon_edge_length_avg(res, unit) |
Unit parameters (v4): Area and length functions now take a unit text parameter:
h3_get_hexagon_area_avg(resolution, [unit = 'km^2']) — use 'km^2' or 'm^2'
h3_get_hexagon_edge_length_avg(resolution, [unit = 'km']) — use 'km' or 'm'
h3_cell_area(cell, [unit = 'km^2']) — exact area for specific cell
h3_edge_length(edge, [unit = 'km']) — exact length for specific edge
Spelling change (v4.2.3+): Prefer h3_latlng_to_cell over h3_lat_lng_to_cell (underscore version deprecated). Same for h3_cell_to_latlng, h3_vertex_to_latlng.
Documentation
1---2name: h3-pg3description: PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.4---56# h3-pg PostgreSQL Extension78## Extension Architecture910**Two extensions:**11- `CREATE EXTENSION h3` — Core H3 bindings (indexing, traversal, hierarchy, inspection, `<->` operator). No PostGIS dependency.12- `CREATE EXTENSION h3_postgis` — PostGIS integration (geometry/geography casts, `@` operator, polygon fill, raster helpers). Requires `postgis` and optionally `postgis_raster`.1314## Postgres-Specific H3 Features1516These exist only in h3-pg, not other H3 language bindings.1718**Direct casts** (`h3_postgis`):19- `h3index::geometry` → cell centroid as POINT, SRID 432620- `h3index::geography` → cell centroid as POINT, SRID 43262122**Operators:**23- `a <-> b` — grid distance in cells between two h3index values (`h3`)24- `geom @ resolution` — index geometry/geography at resolution (`h3_postgis`)2526**Inspection and construction helpers** (`h3`):27- `h3_get_index_digit(cell, resolution)` — inspect the digit at a 1-based resolution step28- `h3_construct_cell(resolution, base_cell_number, digits[])` — rebuild a cell from explicit components29- `h3_is_valid_index(index)` — validate any H3 index mode: cell, directed edge, or vertex3031**Directed edge convenience** (`h3`):32- `h3_reverse_directed_edge(edge)` — returns the same directed edge with origin and destination reversed3334**Operator classes** for `h3index`: BTREE, HASH, BRIN, SP-GIST (`h3`)3536**Boundary helpers** (`h3_postgis`):37- `h3_cell_to_boundary_geometry(h3index)` → polygon with SRID 4326, splits at antimeridian38- `h3_cell_to_boundary_geography(h3index)` → geography polygon, splits at antimeridian3940**Polygon fill** (`h3_postgis`):41- `h3_polygon_to_cells(geometry, resolution)` → SETOF h3index42- `h3_polygon_to_cells(geography, resolution)` → SETOF h3index4344**Raster integration** (`h3_postgis`, requires `postgis_raster`):45- `h3_raster_summary(raster, resolution, [band])` — per-cell stats (count, sum, mean, stddev, min, max)46- `h3_raster_summary_clip(...)` — clips raster by cell boundaries47- `h3_raster_summary_centroids(...)` — uses cell centroids, avoids pentagon edge cases48- `h3_raster_summary_subpixel(...)` — for cells smaller than pixels49- `h3_raster_class_summary*(...)` — discrete class counts per cell5051**Tile helper** (`h3_postgis`):52- `h3_get_resolution_from_tile_zoom(z, [max_h3_resolution=15], [min_h3_resolution], [hex_edge_pixels=44], [tile_size=512])` — returns optimal H3 resolution for XYZ tile zoom level `z`, targeting hexagons approximately `hex_edge_pixels` wide on screen5354## Common Pattern Fixes5556| Pattern | DO NOT | DO |57|---------|--------|----|58| Get centroid | `ST_Centroid(h3_cell_to_boundary_geometry(h3))` | `h3::geometry` |59| Get boundary with SRID | `h3_cell_to_boundary(h3)::geometry` | `h3_cell_to_boundary_geometry(h3)` |60| Distance check (meters) | `ST_Distance(a.h3::geography, b.h3::geography) <= N` | `ST_DWithin(a.h3::geography, b.h3::geography, N)` |61| Grid distance (cells) | `h3_distance(a, b)` | `a <-> b` |6263**Antimeridian gotcha:** Cells crossing 180° are split into valid polygons. `ST_Centroid` of a split polygon may fall outside the cell — use `h3::geometry` for centroids instead.6465**Ring traversal gotcha:** Prefer `h3_grid_ring(origin, k)` for rings because it handles pentagon distortion internally. Use `h3_grid_ring_unsafe(origin, k)` only when fail-fast behavior or ring-walk ordering matters.6667## Critical v3 → v4 Renames6869| v3 Name (DO NOT USE) | v4 Replacement |70|---------------------|----------------|71| `h3_geo_to_h3` | `h3_latlng_to_cell` |72| `h3_to_geo` | `h3_cell_to_latlng` |73| `h3_to_geo_boundary` | `h3_cell_to_boundary` |74| `h3_k_ring` | `h3_grid_disk` |75| `h3_k_ring_distances` | `h3_grid_disk_distances` |76| `h3_hex_ring` | `h3_grid_ring_unsafe` |77| `h3_line` | `h3_grid_path_cells` |78| `h3_distance` | `h3_grid_distance` |79| `h3_to_parent` | `h3_cell_to_parent` |80| `h3_to_children` | `h3_cell_to_children` |81| `h3_compact` | `h3_compact_cells` |82| `h3_uncompact` | `h3_uncompact_cells` |83| `h3_polyfill` | `h3_polygon_to_cells` |84| `h3_indexes_are_neighbors` | `h3_are_neighbor_cells` |85| `h3_hex_area` | `h3_get_hexagon_area_avg` |86| `h3_hex_area_km2` / `h3_hex_area_m2` | `h3_get_hexagon_area_avg(res, unit)` |87| `h3_edge_length_km` / `h3_edge_length_m` | `h3_get_hexagon_edge_length_avg(res, unit)` |8889**Unit parameters (v4):** Area and length functions now take a `unit` text parameter:90- `h3_get_hexagon_area_avg(resolution, [unit = 'km^2'])` — use `'km^2'` or `'m^2'`91- `h3_get_hexagon_edge_length_avg(resolution, [unit = 'km'])` — use `'km'` or `'m'`92- `h3_cell_area(cell, [unit = 'km^2'])` — exact area for specific cell93- `h3_edge_length(edge, [unit = 'km'])` — exact length for specific edge9495**Spelling change (v4.2.3+):** Prefer `h3_latlng_to_cell` over `h3_lat_lng_to_cell` (underscore version deprecated). Same for `h3_cell_to_latlng`, `h3_vertex_to_latlng`.9697## Documentation9899- API reference: https://github.com/postgis/h3-pg/blob/main/docs/api.md100- v3→v4 migration: https://h3geo.org/docs/library/migration-3.x/functions