Skip to content

A5 Geospatial Index

Encode lat/lng to a pentagonal cell ID and aggregate, join, or spatially filter by that ID.

263,265
extension loads Β· last 90 days
On this page

Technical Overview

Every cell covers the same area on the globe

Why pentagons, and why equal area

Real A5 cells tiling a globe β€” pentagons of equal area across the visible hemisphere
  • β€’ The problem A5 solves: When you bucket (lon, lat) points into grid cells and compare counts, the comparison is only honest if the cells are the same size. Grids built on lat/lon rectangles or projected squares stretch badly toward the poles, so a count of 100 near the equator and 100 near a pole describe very different densities. A5 cells are equal-area at each resolution by construction, so cross-latitude density comparisons need no per-cell normalization.
  • β€’ Pentagons are the cost of that uniformity: You cannot tile a sphere with regular hexagons alone β€” H3 pays for its hexagons with 12 unavoidable pentagon exceptions at the icosahedral vertices that traversal code must special-case. A5 instead derives its cells from a dodecahedral subdivision, giving pentagons everywhere β€” uniform shape, uniform area, and no exceptional cells to handle. The geometric argument is laid out on the A5 motivation page.
  • β€’ It's a thin binding over the reference library: The DuckDB functions wrap the upstream A5 Rust library, so cell IDs are identical to what the reference implementation produces β€” a DuckDB-computed ID round-trips through the JS/TypeScript library and back unchanged.

How a point becomes a cell ID

  • β€’ From sphere to cell: A point is located on the globe, mapped onto the face of the dodecahedron it falls in, and then placed within that face's recursive pentagonal subdivision down to the requested resolution. Resolution 0 is the small set of base cells covering the whole globe; each finer level subdivides every cell, so cell count grows by a fixed branching factor per level and resolution 30 reaches sub-square-meter precision.
  • β€’ Resolution is packed into the ID: The cell's position in the subdivision hierarchy and its resolution level are encoded together into the single UBIGINT. Because the level lives inside the integer, the resolution can be recovered from a bare cell ID with no side table β€” and a cell at one resolution maps deterministically to its ancestor at any coarser level, which is what makes hierarchical roll-up a pure integer operation rather than a re-encode of the raw points.
  • β€’ Pick the coarsest resolution that still separates your data: Each finer level multiplies the number of distinct cells (and therefore GROUP BY cardinality) by the branching factor. Encode once at the finest resolution you'll ever need; roll up to coarser views in-query for free. The Cookbook and the resolution guide in the details below show the area-per-level trade-off.
  • β€’ Cells are integers first, geometry on demand: Storage and joins use the compact integer. When you need to draw a cell, the boundary is materialized as [lon, lat] vertex pairs that pair with the spatial extension's polygon builders for GeoJSON output β€” the exact pattern is in the Cookbook.

A5 vs H3 vs S2

  • β€’ Cell shape: H3 tiles the world with hexagons (with 12 unavoidable pentagons at the icosahedral vertices). S2 uses curvilinear quadrilaterals derived from a cube projection. A5 uses pentagons throughout, derived from a dodecahedral subdivision β€” see the A5 motivation page for the geometric argument.
  • β€’ Area uniformity: H3 hex area drifts noticeably with latitude β€” same-resolution cells near the poles are smaller than at the equator. A5 cells are equal-area at each resolution by construction, which matters when you're computing densities or comparing counts across very different latitudes. The A5 vs H3 page in the upstream docs walks through the trade.
  • β€’ Neighbor count: Hexagons have a clean 6-neighbor structure that's nice for grid traversal. Pentagons have 5 edge-neighbors, which is slightly less convenient β€” but A5 has no "pentagon exceptions" the way H3 does, so neighborhood queries don't need special-case logic.
  • β€’ Ecosystem maturity: H3 is older and has a much larger ecosystem (Postgres, BigQuery, Snowflake, Spark, plus Uber's first-party libraries). S2 is widely deployed inside Google. A5 is newer β€” the upstream library and specification are at github.com/felixpalmer/a5. If you need the broadest tool support today, H3 is still the safest pick. Reach for A5 specifically when uniform cell area matters.

Deep Dive

Technical Details

Install

INSTALL a5 FROM community;
LOAD a5;

Quick Start

Encode a point as an A5 cell at resolution 10

SELECT a5_lonlat_to_cell(-74.0060, 40.7128, 10) AS nyc_cell;

Aggregate point data into pentagonal buckets

SELECT a5_lonlat_to_cell(longitude, latitude, 10) AS cell_id,
       COUNT(*)                                  AS point_count
FROM   points
GROUP  BY cell_id
ORDER  BY point_count DESC;

Reference

Extension Contents

Quick reference to all available functions and settings organized by category.

Name Description
Cell Properties
a5_cell_area() Returns the area in square meters of an A5 cell at the specified resolution level
a5_cell_to_boundary() Returns the boundary vertices of an A5 cell as a closed ring of [lon, lat] points
a5_get_resolution() Returns the resolution level (0-30) of an A5 cell
Coordinate Conversion
a5_cell_to_lonlat() Returns the center point [longitude, latitude] of an A5 cell
a5_cell_to_spherical() Returns the spherical coordinates [theta, phi] in radians of an A5 cell center
a5_hex_to_u64() Converts an A5 hex string representation to a UBIGINT cell ID
a5_lonlat_to_cell() Converts a longitude/latitude coordinate to an A5 cell at the specified resolution
a5_u64_to_hex() Converts a UBIGINT A5 cell ID to its hex string representation
Hierarchy
a5_cell_to_children() Returns the immediate child A5 cells (one resolution finer)
a5_cell_to_parent() Returns the parent A5 cell at the specified coarser resolution
a5_compact() Compacts a list of A5 cells by merging complete sets of sibling cells into parent cells
a5_get_num_children() Returns the number of child cells at child_resolution that fit within a cell at parent_resolution
a5_uncompact() Expands a compacted list of A5 cells to the specified target resolution
Traversal
a5_grid_disk() Returns all A5 cells within k edge-steps of the given cell (edge adjacency)
a5_grid_disk_vertex() Returns all A5 cells within k vertex-steps of the given cell (vertex adjacency)
a5_spherical_cap() Returns all A5 cells within the specified radius (in meters) of the given cell
Utilities
a5_get_num_cells() Returns the total number of A5 cells at the specified resolution level (0-30)
a5_get_res0_cells() Returns all 12 resolution 0 (root) A5 cells covering the entire globe

API Reference

Function Documentation

Practical Examples

Cookbook

Real-world recipes and patterns for common use cases.

Platform Support

Compatibility

Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.

Quick Facts

Release status Stable
Software License MIT
Pricing Free
Written In Rust
Source Available Yes
View on GitHub
Usage
263,265
loads Β· last 90 days

Platforms

  • Linux x86_64 aarch64
  • Linux (musl) Not available
  • macOS Intel Apple Silicon
  • Windows x86_64
  • WASM eh mvp threads
Compiled binary sizes
Platform Architecture Size
Linux x86_64 4.27 MB
Linux aarch64 3.88 MB
macOS Intel 1.75 MB
macOS Apple Silicon 1.60 MB
Windows x86_64 7.57 MB
WASM eh 151.3 KB
WASM mvp 171.8 KB
WASM threads 147.0 KB

Compressed download size from the Haybarn extension repository.

DuckDB & Haybarn

Release calendar