Spatial / Geo Queries
Spatial queries let you find documents by geographic proximity — within a radius, inside a bounding box, or nearest to a point. Supported on SQLite (via R*Tree virtual tables) and CosmosDB (via native GeoJSON). Other providers throw NotSupportedException.
Spatial Types
Section titled “Spatial Types”GeoPoint
Section titled “GeoPoint”A geographic coordinate using WGS84. Serializes as GeoJSON {"type":"Point","coordinates":[longitude,latitude]}.
public readonly record struct GeoPoint(double Latitude, double Longitude);GeoBoundingBox
Section titled “GeoBoundingBox”A rectangular area for bounding box queries.
public readonly record struct GeoBoundingBox( double MinLatitude, double MinLongitude, double MaxLatitude, double MaxLongitude);SpatialResult<T>
Section titled “SpatialResult<T>”Wraps a document with its computed distance from the query center point.
public class SpatialResult<T> where T : class{ public required T Document { get; init; } public double DistanceMeters { get; init; }}Configuration
Section titled “Configuration”Register which GeoPoint property to use for spatial indexing per document type using MapSpatialProperty:
public class Restaurant{ public string Id { get; set; } = ""; public string Name { get; set; } = ""; public GeoPoint Location { get; set; } public string Cuisine { get; set; } = "";}
var store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")}.MapSpatialProperty<Restaurant>(r => r.Location));You can map multiple types:
var options = new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")}.MapSpatialProperty<Restaurant>(r => r.Location).MapSpatialProperty<Hotel>(h => h.Coordinates);Optional (nullable) locations
Section titled “Optional (nullable) locations”The mapped property may be a nullable GeoPoint? — map it the same way, and a document whose location
is null is simply left out of the spatial index (it still stores and queries normally otherwise). This
is the common case for records where coordinates are optional, e.g. a calendar event that may or may not
have a place attached:
public class CalendarEvent{ public string Id { get; set; } = ""; public string Title { get; set; } = ""; public GeoPoint? Location { get; set; } // optional}
options.MapSpatialProperty<CalendarEvent>(e => e.Location);Inserting or updating an event with a null location does not throw and does not index it; those
documents never appear in WithinRadius/WithinBoundingBox/NearestNeighbors results. Clearing a
previously-set location on update (setting it back to null) purges the stale index entry, so subsequent
spatial queries no longer match it.
Checking Provider Support
Section titled “Checking Provider Support”Not all providers support spatial queries. Check at runtime:
if (store.SupportsSpatial){ var nearby = await store.WithinRadius<Restaurant>(center, 5000);}| Provider | SupportsSpatial |
|---|---|
| SQLite | true (when spatial properties are mapped) |
| SQLCipher | true (inherits SQLite R*Tree support) |
| CosmosDB | true (when spatial properties are mapped) |
| LiteDB | false |
| MySQL | false |
| SQL Server | false |
| PostgreSQL | false |
Queries
Section titled “Queries”WithinRadius
Section titled “WithinRadius”Find documents within a distance (meters) of a center point, ordered by distance ascending. Returns SpatialResult<T> with the computed distance.
var nearby = await store.WithinRadius<Restaurant>( new GeoPoint(45.5231, -122.6765), // Portland, OR 5000); // 5km radius
foreach (var result in nearby){ Console.WriteLine($"{result.Document.Name} — {result.DistanceMeters:N0}m away");}With an additional predicate filter:
var italianNearby = await store.WithinRadius<Restaurant>( new GeoPoint(45.5231, -122.6765), 5000, filter: r => r.Cuisine == "Italian");WithinBoundingBox
Section titled “WithinBoundingBox”Find documents within a rectangular geographic area.
var inArea = await store.WithinBoundingBox<Restaurant>( new GeoBoundingBox(45.0, -123.0, 46.0, -122.0));With a filter:
var inArea = await store.WithinBoundingBox<Restaurant>( new GeoBoundingBox(45.0, -123.0, 46.0, -122.0), filter: r => r.Cuisine == "Italian");NearestNeighbors
Section titled “NearestNeighbors”Find the K closest documents to a point, ordered by distance ascending.
var closest = await store.NearestNeighbors<Restaurant>( new GeoPoint(45.5231, -122.6765), count: 10);
foreach (var result in closest){ Console.WriteLine($"{result.Document.Name} — {result.DistanceMeters:N0}m");}With a filter:
var closestItalian = await store.NearestNeighbors<Restaurant>( new GeoPoint(45.5231, -122.6765), count: 5, filter: r => r.Cuisine == "Italian");How It Works
Section titled “How It Works”SQLite — R*Tree Sidecar
Section titled “SQLite — R*Tree Sidecar”SQLite uses R*Tree virtual tables for spatial indexing. For each table with spatial-mapped types, the library creates two companion tables:
{table}_spatial_map— maps document text IDs to integer rowids (R*Tree requires integer keys){table}_spatial— the R*Tree index storing latitude/longitude bounding boxes
These tables are created automatically when the table is first used. CRUD operations automatically sync the spatial index:
| Operation | Spatial Sync |
|---|---|
| Insert / Update / Upsert | Extracts GeoPoint from document and upserts into R*Tree |
| Remove | Deletes spatial entry for that document |
| Clear | Removes all spatial entries for that type |
Radius queries use a two-phase approach: the R*Tree provides a fast bounding box pre-filter, then a Haversine post-filter computes exact distances for precision.
Nearest neighbor queries use an expanding bounding box strategy — starting at 10km and doubling until enough candidates are found.
CosmosDB — Native GeoJSON
Section titled “CosmosDB — Native GeoJSON”CosmosDB has built-in geospatial support. The GeoPoint type serializes as GeoJSON directly in the document:
{ "id": "rest-1", "typeName": "Restaurant", "data": { "name": "Pizzeria Roma", "location": { "type": "Point", "coordinates": [-122.6765, 45.5231] } }}CosmosDB automatically indexes GeoJSON properties. The library adds spatial index policies to the container during initialization. Queries use native SQL functions:
- WithinRadius:
ST_DISTANCE(c.data.location, point) <= @radius - WithinBoundingBox:
ST_WITHIN(c.data.location, polygon) - NearestNeighbors:
ORDER BY ST_DISTANCE(c.data.location, point) OFFSET 0 LIMIT @count
No sidecar tables or manual sync needed — CosmosDB handles everything natively.