Geospatial¶
IndentiaDB supports GeoSPARQL — the OGC/W3C standard for spatial querying of RDF data. Geometries are stored as RDF literals with type geo:wktLiteral, indexed with 60-bit precision, and queried with standard GeoSPARQL functions.
60-Bit Geospatial Encoding¶
IndentiaDB encodes each geometry's bounding-box centroid as a 60-bit integer by interleaving 30 bits of latitude and 30 bits of longitude. This encoding:
- Fits in a single 64-bit machine word for fast comparison.
- Provides ~1cm precision at the equator (2^30 subdivisions over ±90°/±180°).
- Maps naturally to the existing key-value index as a range-scannable spatial prefix.
Point lookups use the exact 60-bit code. Range-based spatial queries (within bounding box, distance radius) are implemented as key-range scans over the encoded space, followed by precise geometric refinement using WKT.
Supported Geometry Types
POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, and GEOMETRYCOLLECTION are all supported in WKT format. All coordinates are in WGS 84 (EPSG:4326) — longitude first, latitude second, matching the GeoSPARQL standard.
WKT (Well-Known Text) Parsing¶
Geometries are stored as WKT literals typed as geo:wktLiteral:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX ex: <http://example.org/>
INSERT DATA {
ex:amsterdam
a ex:City ;
ex:name "Amsterdam" ;
geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POINT(4.9 52.37)"^^geo:wktLiteral
] .
ex:rotterdam
a ex:City ;
ex:name "Rotterdam" ;
geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POINT(4.48 51.92)"^^geo:wktLiteral
] .
ex:port_area
a ex:Zone ;
ex:name "Port of Rotterdam" ;
geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POLYGON((4.38 51.88, 4.58 51.88, 4.58 51.96, 4.38 51.96, 4.38 51.88))"^^geo:wktLiteral
] .
}
Nearest Neighbor Query¶
Find the N nearest places to a reference point:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?place ?name ?distance WHERE {
?place a ex:City ;
ex:name ?name ;
geo:hasGeometry ?geom .
?geom geo:asWKT ?wkt .
BIND(geof:distance(?wkt, "POINT(4.9 52.37)"^^geo:wktLiteral, "km") AS ?distance)
FILTER(?distance < 100)
}
ORDER BY ?distance
LIMIT 10
Distance Join Query¶
Join two sets of geometries by spatial proximity:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?store ?hospital ?distance WHERE {
# All stores
?store a ex:Store ;
geo:hasGeometry/geo:asWKT ?storeWkt .
# All hospitals
?hospital a ex:Hospital ;
geo:hasGeometry/geo:asWKT ?hospitalWkt .
# Compute distance
BIND(geof:distance(?storeWkt, ?hospitalWkt, "km") AS ?distance)
FILTER(?distance < 2.0)
}
ORDER BY ?store ?distance
This returns every (store, hospital) pair where the hospital is within 2 km of the store.
GeoSPARQL Functions Reference¶
| Function | Description | Example |
|---|---|---|
geof:distance(geom1, geom2, units) |
Distance between geometries | geof:distance(?wkt1, ?wkt2, "km") |
geof:buffer(geom, radius, units) |
Buffer polygon around geometry | geof:buffer(?wkt, 1.5, "km") |
geof:within(geom1, geom2) |
True if geom1 is within geom2 | FILTER(geof:within(?point, ?polygon)) |
geof:contains(geom1, geom2) |
True if geom1 contains geom2 | FILTER(geof:contains(?region, ?store)) |
geof:intersects(geom1, geom2) |
True if geometries intersect | FILTER(geof:intersects(?route, ?zone)) |
geof:touches(geom1, geom2) |
True if geometries share a boundary | FILTER(geof:touches(?parcel1, ?parcel2)) |
geof:overlaps(geom1, geom2) |
True if geometries overlap | FILTER(geof:overlaps(?zone1, ?zone2)) |
geof:disjoint(geom1, geom2) |
True if geometries have no intersection | FILTER(geof:disjoint(?area, ?exclusionZone)) |
geof:union(geom1, geom2) |
Union of two geometries | BIND(geof:union(?geom1, ?geom2) AS ?merged) |
geof:envelope(geom) |
Bounding box of a geometry | BIND(geof:envelope(?polygon) AS ?bbox) |
geof:centroid(geom) |
Centroid point | BIND(geof:centroid(?polygon) AS ?center) |
Unit strings: "km", "m", "mi", "ft", "deg" (decimal degrees).
SPARQL GeoSPARQL Query: Places Within 50km¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?place ?name ?distance WHERE {
?place a ex:Place ;
ex:name ?name ;
geo:hasGeometry ?geom .
?geom geo:asWKT ?wkt .
BIND(geof:distance(?wkt, "POINT(4.9 52.37)"^^geo:wktLiteral, "km") AS ?distance)
FILTER(?distance < 50)
}
ORDER BY ?distance
SPARQL GeoSPARQL Query: Points Within a Polygon¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?sensor ?name WHERE {
?sensor a ex:Sensor ;
ex:name ?name ;
geo:hasGeometry/geo:asWKT ?sensorWkt .
FILTER(geof:within(
?sensorWkt,
"POLYGON((4.8 52.3, 5.1 52.3, 5.1 52.5, 4.8 52.5, 4.8 52.3))"^^geo:wktLiteral
))
}
CLI: Compute Distance Between Two Points¶
indentiagraph query geo \
--profile dev \
--function geof:distance \
--arg '{"wkt":"POINT(4.9 52.37)"}' \
--arg '{"wkt":"POINT(5.12 52.09)"}' \
--unit km
Output:
Other CLI examples:
# Buffer a point by 1km (returns WKT polygon)
indentiagraph query geo \
--profile dev \
--function geof:buffer \
--arg '{"wkt":"POINT(4.9 52.37)"}' \
--arg '{"radius": 1.0, "unit": "km"}'
# Check if a point is within a polygon
indentiagraph query geo \
--profile dev \
--function geof:within \
--arg '{"wkt":"POINT(4.9 52.37)"}' \
--arg '{"wkt":"POLYGON((4.8 52.3, 5.1 52.3, 5.1 52.5, 4.8 52.5, 4.8 52.3))"}'
Python Integration Example¶
import requests
import json
SPARQL_ENDPOINT = "http://localhost:7001/sparql"
UPDATE_ENDPOINT = "http://localhost:7001/update"
def insert_location(uri: str, name: str, lon: float, lat: float):
"""Insert a named location with a WKT point geometry."""
sparql_update = f"""
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX ex: <http://example.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
INSERT DATA {{
<{uri}>
a ex:Location ;
rdfs:label "{name}" ;
geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POINT({lon} {lat})"^^geo:wktLiteral
] .
}}
"""
resp = requests.post(
UPDATE_ENDPOINT,
headers={"Content-Type": "application/sparql-update"},
data=sparql_update,
)
resp.raise_for_status()
def find_nearby(lon: float, lat: float, radius_km: float) -> list[dict]:
"""Find all locations within radius_km of the given coordinates."""
sparql_query = f"""
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ex: <http://example.org/>
SELECT ?location ?name ?distance WHERE {{
?location a ex:Location ;
rdfs:label ?name ;
geo:hasGeometry/geo:asWKT ?wkt .
BIND(geof:distance(?wkt, "POINT({lon} {lat})"^^geo:wktLiteral, "km") AS ?distance)
FILTER(?distance < {radius_km})
}}
ORDER BY ?distance
"""
resp = requests.post(
SPARQL_ENDPOINT,
headers={
"Content-Type": "application/sparql-query",
"Accept": "application/sparql-results+json",
},
data=sparql_query,
)
resp.raise_for_status()
bindings = resp.json()["results"]["bindings"]
return [
{
"uri": b["location"]["value"],
"name": b["name"]["value"],
"distance": float(b["distance"]["value"]),
}
for b in bindings
]
# Insert some locations
insert_location("http://example.org/dam", "Dam Square", 4.8952, 52.3731)
insert_location("http://example.org/rijksmuseum", "Rijksmuseum", 4.8852, 52.3600)
insert_location("http://example.org/anne_frank", "Anne Frank House", 4.8839, 52.3752)
insert_location("http://example.org/central_station","Amsterdam Centraal", 4.9000, 52.3791)
# Find everything within 1km of the Dam
nearby = find_nearby(lon=4.8952, lat=52.3731, radius_km=1.0)
print(f"Found {len(nearby)} locations within 1km:")
for loc in nearby:
print(f" {loc['name']}: {loc['distance']:.3f} km")
Configuration¶
[geospatial]
enabled = true
default_crs = "http://www.opengis.net/def/crs/OGC/1.3/CRS84" # WGS84
precision_bits = 60 # bits for geohash (30 lat + 30 lon)
max_distance_km = 20000 # Maximum allowed distance in FILTER clauses
[geospatial.index]
type = "geohash" # "geohash" | "r-tree" (future)
cell_size_meters = 1 # ~1cm precision at 60 bits
Coordinate Order: Longitude First
GeoSPARQL and WKT use longitude, latitude order (X, Y in Cartesian terms). This is the opposite of GPS coordinate display conventions (which show latitude first). POINT(4.9 52.37) means longitude=4.9°E, latitude=52.37°N (Amsterdam).
R-Tree Indexing¶
In addition to the default 60-bit geohash encoding, IndentiaDB supports R-tree spatial indexing for complex geometry queries. The R-tree is built on the rstar crate and provides efficient operations on bounding boxes (axis-aligned bounding boxes / AABBs).
How the R-Tree Works¶
Each geometry is stored alongside its bounding-box envelope in the R-tree. The tree organizes envelopes hierarchically:
- Leaf nodes contain geometry entries with their bounding boxes.
- Internal nodes contain the minimum bounding rectangle (MBR) that encloses all children.
- The tree is balanced, providing O(log n) insertion, deletion, and query performance.
Query Types¶
| Query | Operation | Complexity |
|---|---|---|
| Range query | Find all geometries whose bounding box intersects a query envelope | O(log n + k) |
| K-nearest neighbor | Find the K closest geometries to a point | O(log n · k) |
| Point containment | Find all polygons containing a point | O(log n + k) |
Bulk Loading¶
When loading large datasets, use bulk loading for efficient index construction. Bulk loading sorts geometries by their spatial locality before building the tree, resulting in better node utilization:
# Import with bulk-loaded spatial index
indentiagraph import \
--profile prod \
--input places.ttl \
--spatial-index rtree \
--bulk-load
Geohash vs. R-Tree¶
| Feature | Geohash (default) | R-Tree |
|---|---|---|
| Point lookups | Fast (exact code match) | O(log n) |
| Range scans | Key-range scan + refinement | Bounding box intersection |
| Complex polygons | Requires WKT refinement | Native envelope support |
| KNN queries | Approximate via cell expansion | Exact via tree traversal |
| Memory overhead | Minimal (64-bit per point) | Higher (tree structure) |
| Recommended for | Point-heavy datasets | Mixed geometry types |
Buffer Operations¶
Buffer operations create a polygon around a geometry at a specified distance, useful for proximity zones and area-of-influence queries.
Creating Buffer Zones¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
# Find all residential areas within 500m of an industrial zone
SELECT ?area ?name WHERE {
ex:industrial_zone geo:hasGeometry/geo:asWKT ?zoneWkt .
# Create a 500m buffer around the industrial zone
BIND(geof:buffer(?zoneWkt, 0.5, "km") AS ?buffer)
?area a ex:ResidentialArea ;
ex:name ?name ;
geo:hasGeometry/geo:asWKT ?areaWkt .
FILTER(geof:intersects(?areaWkt, ?buffer))
}
Multi-Ring Buffer Analysis¶
Create concentric buffer zones for impact analysis:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?facility ?zone ?count WHERE {
?facility a ex:ChemicalPlant ;
geo:hasGeometry/geo:asWKT ?facilityWkt .
VALUES (?zone ?radius) {
("immediate" 1.0)
("warning" 5.0)
("advisory" 10.0)
}
BIND(geof:buffer(?facilityWkt, ?radius, "km") AS ?buffer)
{
SELECT ?buffer (COUNT(?resident) AS ?count) WHERE {
?resident a ex:Resident ;
geo:hasGeometry/geo:asWKT ?residentWkt .
FILTER(geof:within(?residentWkt, ?buffer))
}
GROUP BY ?buffer
}
}
Spatial Aggregations¶
Counting Points in Polygons¶
Count the number of sensors in each monitoring zone:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?zone ?zone_name (COUNT(?sensor) AS ?sensor_count) WHERE {
?zone a ex:MonitoringZone ;
ex:name ?zone_name ;
geo:hasGeometry/geo:asWKT ?zoneWkt .
OPTIONAL {
?sensor a ex:Sensor ;
geo:hasGeometry/geo:asWKT ?sensorWkt .
FILTER(geof:within(?sensorWkt, ?zoneWkt))
}
}
GROUP BY ?zone ?zone_name
ORDER BY DESC(?sensor_count)
Area Calculations¶
Compute the area of polygons using the geof:area function:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?parcel ?name ?area_sqkm WHERE {
?parcel a ex:LandParcel ;
ex:name ?name ;
geo:hasGeometry/geo:asWKT ?wkt .
BIND(geof:area(?wkt, "km2") AS ?area_sqkm)
}
ORDER BY DESC(?area_sqkm)
Supported area units: "m2" (square meters), "km2" (square kilometers), "ha" (hectares), "ac" (acres).
Multi-CRS Support¶
IndentiaDB defaults to WGS 84 (EPSG:4326) but also supports Web Mercator (EPSG:3857) for web mapping applications. CRS transformation is handled transparently.
Supported Coordinate Reference Systems¶
| CRS | EPSG Code | Use Case |
|---|---|---|
| WGS 84 | 4326 | Geographic coordinates (default) |
| Web Mercator | 3857 | Web mapping (OpenStreetMap, Google Maps) |
CRS Mismatch Detection¶
When combining geometries from different CRS, IndentiaDB detects the mismatch and automatically transforms coordinates. This prevents silent errors from comparing incompatible coordinate spaces.
Explicit CRS Declaration¶
Declare CRS in WKT using the GeoSPARQL CRS URI:
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX ex: <http://example.org/>
INSERT DATA {
ex:building_footprint
a geo:Geometry ;
geo:asWKT "<http://www.opengis.net/def/crs/EPSG/0/3857> POLYGON((544250 6867280, 544350 6867280, 544350 6867380, 544250 6867380, 544250 6867280))"^^geo:wktLiteral .
}
Queries mixing CRS84 and EPSG:3857 geometries will transparently transform to a common CRS before computing spatial relations.
WGS 84 to Web Mercator Limits¶
Web Mercator is undefined beyond approximately +/-85.05 degrees latitude. Geometries at extreme latitudes (e.g., polar regions) must remain in WGS 84.
Geospatial + Temporal: Moving Objects¶
Combine spatial and temporal queries to track objects that change location over time:
Tracking Vehicle Positions¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
# Insert timestamped positions
INSERT DATA {
TEMPORAL VALID "2026-03-23T08:00:00Z" TO "2026-03-23T08:15:00Z" {
ex:truck_42 geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POINT(4.90 52.37)"^^geo:wktLiteral
] .
}
}
INSERT DATA {
TEMPORAL VALID "2026-03-23T08:15:00Z" TO "2026-03-23T08:30:00Z" {
ex:truck_42 geo:hasGeometry [
a geo:Geometry ;
geo:asWKT "POINT(4.48 51.92)"^^geo:wktLiteral
] .
}
}
Query: Where Was a Vehicle at a Given Time?¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX ex: <http://example.org/>
SELECT ?wkt WHERE {
TEMPORAL AS OF VALID "2026-03-23T08:10:00Z"
ex:truck_42 geo:hasGeometry/geo:asWKT ?wkt .
}
Query: Which Vehicles Entered a Zone During a Time Window?¶
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX ex: <http://example.org/>
SELECT ?vehicle ?arrival WHERE {
TEMPORAL BETWEEN VALID "2026-03-23T06:00:00Z" AND "2026-03-23T18:00:00Z"
?vehicle a ex:Vehicle ;
geo:hasGeometry/geo:asWKT ?vehicleWkt .
BIND(TEMPORAL_START(?vehicle) AS ?arrival)
FILTER(geof:within(
?vehicleWkt,
"POLYGON((4.38 51.88, 4.58 51.88, 4.58 51.96, 4.38 51.96, 4.38 51.88))"^^geo:wktLiteral
))
}
ORDER BY ?arrival
Performance Tuning¶
Index Configuration¶
[geospatial]
enabled = true
[geospatial.index]
type = "geohash" # "geohash" for point-heavy, "r-tree" for polygons
precision_bits = 60 # Default 60; lower values = faster range scans, less precision
cell_size_meters = 1 # Minimum cell resolution
[geospatial.performance]
use_spatial_index = true # Always true unless debugging
max_vertices = 10000 # Reject overly complex geometries
Query Optimization Tips¶
-
Add bounding-box pre-filters: For distance queries over large datasets, add a coarse bounding-box filter before the precise distance calculation:
-
Limit result sets: Always use
LIMITon spatial queries to avoid scanning the entire index. -
Use
geof:withinovergeof:distancewhen possible: Polygon containment checks are faster than distance calculations because they avoid trigonometric functions. -
Validate geometry complexity: The
max_verticessetting (default 10,000) rejects overly complex geometries on insert. For datasets with high-resolution coastlines or boundaries, consider simplifying geometries before import. -
Monitor index size: Check spatial index statistics: