Geospatial RAG Pipelines

Retrieval-augmented generation lets a spatial LLM answer grounded questions by pulling supporting documents at inference time, but the geographic…

Articles in this topic

Retrieval-augmented generation lets a spatial LLM answer grounded questions by pulling supporting documents at inference time, but the geographic dimension introduces a failure mode that plain-text RAG never sees: the retriever returns a passage that is semantically on-topic yet spatially wrong — coordinates in the wrong reference system, a region a thousand kilometres from the query, or a polygon severed mid-ring by a naive text splitter. That poisoned context flows straight into generation, and the model confidently emits fabricated coordinates. This area treats retrieval as a spatial correctness problem, not just a similarity problem, so that every retrieved chunk carries a verifiable CRS, a bounding box, and topology intact enough to reason over.

Geospatial retrieval pipeline Spatial corpus indexing, bbox-aware retrieval, CRS resolution, spatial reranking, and grounded generation run in sequence so the language model receives context that is both relevant and geographically correct. Corpusindexing Bboxretrieval CRSresolution Spatialreranking Groundedgeneration Context that is relevant AND geographically correct Guards against: wrong CRS · wrong region · severed polygons

Spatial RAG sits alongside two related areas of this site. The upstream mechanics of encoding geometry — how a shape becomes a vector — belong to Spatial LLM Architecture & Core Concepts, and the downstream act of turning a grounded answer into executable spatial operations belongs to Geospatial Prompt Engineering & Tool Routing. Retrieval is the connective tissue between them: it decides which geographic facts ever reach the model.

Choosing a store that filters by geography, not just similarity

A vector database that ranks purely on embedding distance will happily return the top-k nearest neighbours regardless of where on Earth they sit. When a query is scoped to a metropolitan boundary, unconstrained similarity search leaks documents from other continents into the context window. The store must therefore support a metadata and bounding-box pre-filter that runs before the approximate nearest-neighbour scan, narrowing the candidate set to the geographic envelope of interest.

def store_supports_geo_prefilter(store, probe_bbox, probe_vector):
    """Verify a vector store applies a geographic pre-filter before ANN ranking.
    Returns True only if scoped results are a strict subset of unscoped ones."""
    try:
        scoped = store.search(probe_vector, k=25, bbox=probe_bbox)
        unscoped = store.search(probe_vector, k=25)
    except (AttributeError, TypeError) as exc:
        # Store lacks a bbox parameter; treat as unsupported rather than guessing.
        return False
    scoped_ids = {hit.id for hit in scoped}
    unscoped_ids = {hit.id for hit in unscoped}
    if not scoped_ids:
        return False
    return scoped_ids.issubset(unscoped_ids | scoped_ids)

Getting this decision right up front avoids re-indexing an entire corpus later; the trade-offs between engines are worked through in Spatial Vector Store Selection.

Resolving the reference system of retrieved context

Text passages describing coordinates rarely state their CRS explicitly. A chunk may quote eastings and northings from a national grid, latitude and longitude in WGS84, or Web Mercator metres, and all three look like plausible number pairs to a language model. If the pipeline forwards them without resolution, the model averages incompatible frames and emits coordinates that land in the ocean.

def infer_axis_frame(sample_coords):
    """Heuristically flag whether retrieved coordinates are geographic degrees.
    Falls back to 'unknown' so the caller must resolve CRS explicitly."""
    try:
        xs = [float(x) for x, _ in sample_coords]
        ys = [float(y) for _, y in sample_coords]
    except (ValueError, TypeError):
        return "unknown"
    if not xs or not ys:
        return "unknown"
    if all(-180 <= x <= 180 for x in xs) and all(-90 <= y <= 90 for y in ys):
        return "geographic_degrees"
    return "projected_or_unknown"

Because the reference frame must be attached to every retrieved fragment, the retrieval step and the CRS step are inseparable; the resolution strategy is detailed in Retrieval-Augmented CRS Resolution and builds on the ingestion-time work in Coordinate Reference System Normalization.

Splitting spatial documents without severing geometries

Generic recursive text splitters chunk on character counts and paragraph breaks. Applied to a document containing an embedded WKT polygon or a GeoJSON feature collection, they cut a ring in half, leaving one chunk with an unclosed boundary and another with orphaned vertices. Either fragment retrieved alone yields an invalid geometry the model cannot reason about.

def chunk_preserves_geometry(chunk_text):
    """Reject a chunk that opens a geometry literal it does not close."""
    opens = chunk_text.count("POLYGON") + chunk_text.count("MULTIPOLYGON")
    balanced = chunk_text.count("(") == chunk_text.count(")")
    if opens and not balanced:
        return False  # geometry literal was severed at the chunk boundary
    return True

Boundary-aware splitting keeps each retrievable unit self-contained, a discipline expanded in Chunk-Boundary Strategies for Spatial Corpora and connected to Geometry Tokenization Strategies.

Reranking candidates by distance and relevance together

First-stage vector recall optimises for semantic closeness; it does not know that a query about flood risk in one river basin should down-weight an otherwise similar passage about a basin on another continent. A spatial reranker fuses embedding score with a geographic proximity term so that near, relevant context outranks far, relevant context.

import math

def spatial_rerank(candidates, query_lonlat, alpha=0.6):
    """Blend semantic score with inverse geographic distance.
    Returns candidates sorted by fused score; empty input yields an empty list."""
    if not candidates:
        return []
    qx, qy = query_lonlat
    ranked = []
    for c in candidates:
        try:
            dx = (c.lon - qx) * math.cos(math.radians(qy))
            dy = c.lat - qy
            dist_deg = math.hypot(dx, dy)
        except AttributeError:
            dist_deg = float("inf")  # missing geometry sinks to the bottom
        proximity = 1.0 / (1.0 + dist_deg)
        fused = alpha * c.score + (1 - alpha) * proximity
        ranked.append((fused, c))
    ranked.sort(key=lambda pair: pair[0], reverse=True)
    return [c for _, c in ranked]

Fusing the two signals is the last correctness gate before the prompt is assembled, and it is developed fully in Spatial Context Retrieval & Reranking.

Measuring whether retrieval actually grounded the answer

None of the preceding stages is trustworthy without measurement. A grounded spatial answer must be checked against the coordinates that were actually retrieved: if the model emits a point outside the union of retrieved bounding boxes, retrieval failed to constrain it and the output should be quarantined. Treating this as a scored metric rather than a spot check turns regressions into build failures.

def answer_is_grounded(answer_points, retrieved_bboxes):
    """Confirm every emitted coordinate falls inside some retrieved envelope."""
    if not retrieved_bboxes:
        return False  # nothing was retrieved; the answer cannot be grounded
    for px, py in answer_points:
        inside = any(minx <= px <= maxx and miny <= py <= maxy
                     for (minx, miny, maxx, maxy) in retrieved_bboxes)
        if not inside:
            return False
    return True

Systematic scoring of grounding, hallucinated coordinates, and geometric fidelity is the remit of Evaluation & Benchmarking for Spatial LLMs.

Production Engineering Standards

  1. Every retrieved chunk carries an explicit CRS tag; a chunk with an unresolved reference frame is never placed in the context window.
  2. The vector store applies a bounding-box and metadata pre-filter before ANN ranking — never a post-filter that silently collapses recall.
  3. Geometry literals (WKT, GeoJSON) are never split across chunk boundaries; a splitter that opens a ring must close it in the same unit.
  4. Reranking fuses semantic score with a geographic proximity term; distance is a first-class ranking signal, not an afterthought.
  5. Generation is gated: any coordinate the model emits outside the union of retrieved bounding boxes triggers a deterministic fallback and is not returned.
  6. Retrieval latency, geographic recall, and grounding rate are emitted as observability KPIs on every request, with alert thresholds wired into CI regression harnesses.
  7. Token budgets account for geometry verbosity; high-vertex polygons are downsampled or summarised before entering the prompt, never truncated mid-coordinate.