Skip to main content

H3 Indexing

H3 is Uber's hexagonal hierarchical spatial index. Hexagons provide the most uniform coverage of any regular polygon that tiles the plane, with consistent neighbor distances in all directions.

How H3 Works

H3 divides the Earth into hexagonal cells at multiple resolutions. Each hexagon has exactly 6 neighbors (except 12 pentagons at icosahedron vertices), and each parent cell contains 7 children (aperture-7 subdivision).

Entity Definition

using Oproto.FluentDynamoDb.Attributes;
using Oproto.FluentDynamoDb.Geospatial;

[DynamoDbTable("delivery_zones")]
public partial class DeliveryZone
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string ZoneId { get; set; } = string.Empty;

[DynamoDbAttribute("center", SpatialIndexType = SpatialIndexType.H3, H3Resolution = 9)]
public GeoLocation Center { get; set; }

[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
}

H3 Resolutions

ResolutionHexagon EdgeUse Case
5~8.5 kmCity-wide searches
6~3.2 kmDistrict searches
7~1.2 kmNeighborhood searches
8~460 mLocal area searches
9~174 mDefault — Street-level searches
10~66 mBuilding-level searches
11~25 mPrecise location tracking

Querying

Non-Paginated (Fastest)

var center = new GeoLocation(37.7749, -122.4194);

var result = await zoneTable.SpatialQueryAsync(
spatialAttributeName: "center",
center: center,
radiusKilometers: 5,
queryBuilder: (query, cell, pagination) => query
.Where<DeliveryZone>(x => x.PartitionKey == "ZONE" && x.Center == cell)
.Paginate(pagination),
pageSize: null // All cells queried in parallel
);

foreach (var zone in result.Items)
{
var distance = zone.Center.DistanceToKilometers(center);
Console.WriteLine($"{zone.Name}: {distance:F2}km away");
}

Paginated

var result = await zoneTable.SpatialQueryAsync(
spatialAttributeName: "center",
center: center,
radiusKilometers: 10,
queryBuilder: (query, cell, pagination) => query
.Where<DeliveryZone>(x => x.PartitionKey == "ZONE" && x.Center == cell)
.Paginate(pagination),
pageSize: 50
);

Console.WriteLine($"Found {result.Items.Count} zones (page 1)");
Console.WriteLine($"Cells queried: {result.TotalCellsQueried}");
Console.WriteLine($"Has more: {result.ContinuationToken != null}");

Working with H3 Cells Directly

using Oproto.FluentDynamoDb.Geospatial.H3;

var location = new GeoLocation(37.7749, -122.4194);

// Convert to H3 cell
var cell = location.ToH3Cell(resolution: 9);
Console.WriteLine($"H3 Index: {cell.Index}");
Console.WriteLine($"Resolution: {cell.Resolution}");

// Get neighboring cells (6 neighbors for hexagons)
var neighbors = cell.GetNeighbors();

// Get parent cell (lower resolution)
var parent = cell.GetParent(); // Resolution 8

// Get child cells (7 children, aperture-7)
var children = cell.GetChildren(); // Resolution 10

When to Use H3

Best for:

  • Most uniform coverage needed (hexagons tile more uniformly than squares)
  • Neighbor analysis (6 equidistant neighbors vs 8 non-equidistant for squares)
  • Grid analysis and heatmap generation
  • Delivery territory optimization
  • Ride-sharing zone definitions

Key advantage over S2: Hexagonal cells have neighbors at consistent distances in all directions. Square cells (S2) have diagonal neighbors that are √2 farther than edge neighbors.

Compared to GeoHash:

  • Much better area uniformity
  • Multiple queries instead of single BETWEEN
  • Same latency in non-paginated mode (parallel execution)

Precision Selection

Match H3 resolution to your typical search radius:

Search RadiusRecommended ResolutionApproximate Cell Count
1-5 km8-1010-50
5-10 km7-820-100
10-50 km5-750-200
50-100 km4-5100-300
Query Explosion

H3 has 7 children per cell (vs 4 for S2), so cell counts grow faster at higher resolutions. A 10km radius with Resolution 9 (~174m) generates ~13,000 cells. Use Resolution 7-8 for 10km queries.

Real-World Example: Delivery Zone Checker

public class DeliveryService
{
private readonly DeliveryZoneTable _zoneTable;

public async Task<DeliveryZoneInfo> CheckDeliveryAvailability(
double latitude, double longitude)
{
var location = new GeoLocation(latitude, longitude);

var result = await _zoneTable.SpatialQueryAsync(
spatialAttributeName: "center",
center: location,
radiusKilometers: 10,
queryBuilder: (query, cell, pagination) => query
.Where<DeliveryZone>(x => x.PartitionKey == "ZONE" && x.Center == cell)
.Paginate(pagination),
pageSize: null
);

var zone = result.Items
.Where(z => location.DistanceToKilometers(z.Center) <= z.RadiusKm)
.OrderBy(z => location.DistanceToKilometers(z.Center))
.FirstOrDefault();

return new DeliveryZoneInfo
{
IsAvailable = zone != null,
ZoneName = zone?.Name,
DistanceFromCenter = zone != null
? location.DistanceToKilometers(zone.Center)
: null
};
}
}

See Also