Skip to main content

GeoHash Indexing

GeoHash is the simplest spatial indexing system, encoding geographic coordinates into a sortable string that enables efficient DynamoDB BETWEEN queries.

How GeoHash Works

GeoHash divides the Earth into a grid of rectangular cells using a Z-order curve. Each character in the hash narrows the area by a factor of 32, providing progressively finer precision.

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

string hash = sanFrancisco.ToGeoHash(7); // "9q8yy9r"
// Each character narrows the area:
// "9" → ~2500km × 5000km
// "9q" → ~630km × 1250km
// "9q8" → ~78km × 156km
// "9q8y" → ~20km × 39km
// "9q8yy" → ~2.4km × 4.9km
// "9q8yy9" → ~610m × 1.2km
// "9q8yy9r" → ~76m × 153m

Entity Definition

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

[DynamoDbTable("stores")]
public partial class Store
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string StoreId { get; set; } = string.Empty;

[DynamoDbAttribute("location", GeoHashPrecision = 7)]
public GeoLocation Location { get; set; }

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

Precision Levels

PrecisionCell SizeUse Case
4~20 km × 39 kmCity-wide searches
5~2.4 km × 4.9 kmNeighborhood searches
6~610 m × 1.2 kmDefault — District searches
7~76 m × 153 mStreet-level searches
8~19 m × 38 mBuilding-level searches
9~4.8 m × 4.8 mPrecise location tracking

Querying

Lambda Expression Queries

using Oproto.FluentDynamoDb.Geospatial.GeoHash;

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

// Find stores within 5 kilometers
var nearbyStores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, 5))
.ExecuteAsync();

// Sort by actual distance (DynamoDB can't sort by distance)
var sorted = nearbyStores
.OrderBy(s => s.Location.DistanceToKilometers(center))
.ToList();

All Distance Units

// Meters
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceMeters(center, 5000))
.ExecuteAsync();

// Kilometers
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, 5))
.ExecuteAsync();

// Miles
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceMiles(center, 3.1))
.ExecuteAsync();

Bounding Box Queries

var southwest = new GeoLocation(37.7, -122.5);
var northeast = new GeoLocation(37.8, -122.4);

var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinBoundingBox(southwest, northeast))
.ExecuteAsync();

Performance

GeoHash uses a single BETWEEN query regardless of search radius, making it the fastest option:

ScenarioQuery CountLatency
Any radius, any precision1 query~50ms

This is because GeoHash encodes to a sortable string, enabling a single range scan.

When to Use GeoHash

Best for:

  • Simple proximity queries
  • Low latency requirements (single query)
  • Mid-latitude locations (between ±60°)
  • Backward compatibility with existing GeoHash data

Limitations:

  • Rectangular cells (not uniform area)
  • Poor precision near poles (cells become elongated)
  • Cell boundary issues (nearby items may be in different cells)
  • Queries return rectangular areas, not circles

Post-Filtering for Exact Distances

GeoHash queries return rectangular bounding boxes. Always post-filter for exact circular distances:

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

// Query returns rectangular area
var candidates = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, radiusKm))
.ExecuteAsync();

// Post-filter for exact circular distance
var exactResults = candidates
.Where(s => s.Location.DistanceToKilometers(center) <= radiusKm)
.OrderBy(s => s.Location.DistanceToKilometers(center))
.ToList();

See Also