Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

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.

A geographic coordinate using WGS84. Serializes as GeoJSON {"type":"Point","coordinates":[longitude,latitude]}.

public readonly record struct GeoPoint(double Latitude, double Longitude);

A rectangular area for bounding box queries.

public readonly record struct GeoBoundingBox(
double MinLatitude, double MinLongitude,
double MaxLatitude, double MaxLongitude);

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; }
}

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);

Not all providers support spatial queries. Check at runtime:

if (store.SupportsSpatial)
{
var nearby = await store.WithinRadius<Restaurant>(center, 5000);
}
ProviderSupportsSpatial
SQLitetrue (when spatial properties are mapped)
SQLCiphertrue (inherits SQLite R*Tree support)
CosmosDBtrue (when spatial properties are mapped)
LiteDBfalse
MySQLfalse
SQL Serverfalse
PostgreSQLfalse

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");

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");

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");

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:

OperationSpatial Sync
Insert / Update / UpsertExtracts GeoPoint from document and upserts into R*Tree
RemoveDeletes spatial entry for that document
ClearRemoves 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 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.