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
| Resolution | Hexagon Edge | Use Case |
|---|---|---|
| 5 | ~8.5 km | City-wide searches |
| 6 | ~3.2 km | District searches |
| 7 | ~1.2 km | Neighborhood searches |
| 8 | ~460 m | Local area searches |
| 9 | ~174 m | Default — Street-level searches |
| 10 | ~66 m | Building-level searches |
| 11 | ~25 m | Precise 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 Radius | Recommended Resolution | Approximate Cell Count |
|---|---|---|
| 1-5 km | 8-10 | 10-50 |
| 5-10 km | 7-8 | 20-100 |
| 10-50 km | 5-7 | 50-200 |
| 50-100 km | 4-5 | 100-300 |
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
- Geospatial Overview — Installation and configuration
- GeoHash — Simpler single-query approach
- S2 Cells — Square cell alternative
- Query Optimization — Performance tuning and precision selection