Skip to main content

GeoSpark

Verifiable spatial reasoning for any LLM.

Distances, topology and CRS computed by a geometry engine, not guessed by a language model. Open source, runs anywhere, works with any model that can call a tool.

Tests PyPI Python Docker License


Ask an LLM "How far is the Eiffel Tower from the Louvre?" or "Is this point inside that polygon?" and the answer depends on which model you picked. Across 9 models we benchmarked, small open-weight models (3.8B–9B) got numeric distance questions wrong about 95% of the time. Frontier APIs ranged from 25% (Gemini 2.5 Pro) to 95% (GPT-5.4) correct.

GeoSpark gives the model tools that compute the answer: geodesic distances on the WGS84 ellipsoid, exact topology, CRS transformations, geocoding, elevation and live environmental data. With them, every model that made valid tool calls answered 76–96% of the same questions correctly. A 7B model on your own CPU then performs like a paid frontier API, and every answer comes with a deterministic trace you can check.

Try it

No install: the hosted API (open, no key needed; interactive docs)

curl -X POST https://geospark.terrascout.app/api/v1/distance \
  -H "Content-Type: application/json" \
  -d '{"lat_a": 48.8584, "lon_a": 2.2945, "lat_b": 48.8606, "lon_b": 2.3376}'
# {"distance_m": 3172.01, "distance_km": 3.172, "note": "Geodesic distance on WGS84 ellipsoid"}

In Claude Desktop or any MCP client

pip install "geospark-ai[mcp]"

Add GeoSpark to your MCP client's config. For Claude Desktop that's ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, or %APPDATA%\Claude\claude_desktop_config.json on Windows:

{ "mcpServers": { "geospark": { "command": "geospark-mcp" } } }

That gives the model twelve read-only tools. Computation: calculate_distance, check_spatial_relationship and spatial_operation (buffer, area, length, centroid, convex hull). Places and data: geocode, reverse_geocode, get_elevation, route, get_weather, get_air_quality and get_active_fires. Analysis: location_report and select_site.

Results come back as structured content with output schemas, usually GeoJSON. They carry provenance (source, licence, attribution), units and warnings. When GeoSpark can't answer reliably (invalid geometry, unknown place, a data source down), the tool returns an error that says why, so the model can tell the user instead of estimating. The server uses MCP SDK 2.x (protocol 2026-07-28) and still works with clients on older protocol versions.

As a Python library

pip install geospark-ai              # core
pip install "geospark-ai[api,mcp]"   # + REST server and MCP server
pip install "geospark-ai[all]"       # everything

Run your own server with Docker

docker run -p 8000:8000 ghcr.io/maz2580/geospark:latest
# then open http://localhost:8000

That runs in development mode, where operator routes are open: fine on your own machine. For a public server, add -e GEOSPARK_ENV=production -e GEOSPARK_ADMIN_TOKEN=<secret> (see Self-hosting).

What it does

Spatial engine Geodesic distance, area and length (pyproj on WGS84); topology checks (contains, within, intersects, touches, crosses, overlaps, disjoint) via Shapely; shape-preserving buffers; union, intersection, difference, centroid, convex hull; CRS transformation of every input geometry
Places and terrain Geocoding and reverse geocoding (OpenStreetMap Nominatim, following its usage policy: ≤ 1 request/s and cached); elevation from Copernicus GLO-30 with Open Elevation fallback and vertical-datum awareness (NAVD88 / EGM96 / WGS84); routing (OSRM)
Live data Weather and forecasts (Open-Meteo, no key); air quality (OpenAQ v3, free key); active fires (NASA FIRMS, free key); satellite scene search (STAC)
Agents GeoAgent (multi-step analysis from a plain-English goal), SpatialReport (a location dossier: amenities, accessibility, elevation, weather), SiteSelector (multi-criteria site ranking), and a coordinator that routes a goal to the right one
Interfaces Python library, CLI (geospark …), REST API (49 documented operations), MCP server (geospark-mcp), web playground
Memory and workflows Spatial memory with facts, episodes and contradiction detection; a tiered context store; Flows (DAG workflows, buildable from a natural-language goal)
GeoSpark Bench 535 reproducible questions across topology, distance, multi-step reasoning, change and multimodal tasks, with computed ground truth

Planned, not built yet: slope, viewshed, isochrones, raster band math and change detection. These operations exist in the protocol, but today they return a clear "not implemented yet" error rather than an answer. See the roadmap.

Quick start

Ground-truth geometry

from geospark.engine.spatial_reasoner import SpatialReasoner

park = {"type": "Polygon", "coordinates": [[[2.29, 48.85], [2.30, 48.85], [2.30, 48.86], [2.29, 48.86], [2.29, 48.85]]]}
point = {"type": "Point", "coordinates": [2.295, 48.855]}  # GeoJSON order: [lon, lat]

SpatialReasoner.check_relationship(park, point, "contains")  # True

SpatialReasoner.calculate_distance(
    {"type": "Point", "coordinates": [2.2945, 48.8584]},  # Eiffel Tower
    {"type": "Point", "coordinates": [2.3376, 48.8606]},  # Louvre
)  # 3172.01 (metres, geodesic)

Queries through the engine

from geospark import Engine
from geospark.protocol import SpatialOperation, SpatialQuery

engine = Engine(tools=["geocoder", "terrain"])

place = engine.execute(SpatialQuery(operation=SpatialOperation.GEOCODE,
                                    metadata={"query": "Federation Square, Melbourne"}))
print(place.features[0].geometry)  # {'type': 'Point', 'coordinates': [144.969..., -37.817...]}

# Inputs in another CRS are transformed to WGS84 first, including the second geometry
buffered = engine.execute(SpatialQuery(operation=SpatialOperation.BUFFER,
                                       geometry={"type": "Point", "coordinates": [2.2945, 48.8584]},
                                       radius_m=100))
print(buffered.features[0].properties["area_sq_m"])  # ≈ 31,365

Every answer is checkable, and opens in QGIS

result = engine.execute(SpatialQuery(operation=SpatialOperation.GEOCODE, metadata={"query": "Springfield"}))
result.status        # "ok" | "partial" | "cannot_answer"
result.warnings      # ["Ambiguous place name: 'Springfield, …, Illinois…'; '…Massachusetts…' …"]
result.provenance    # source, licence (ODbL-1.0), attribution, retrieved_at, method
result.spatial_context.units  # {"distance_m": "metre", ...}

import json
json.dump(result.to_geojson(), open("springfield.geojson", "w"))  # RFC 7946, drag into QGIS

GeoSpark refuses rather than guesses. A self-intersecting polygon, out-of-range coordinates or a likely [lat, lon] swap gets cannot_answer with the reason, not a confident wrong number. From the API, add ?format=geojson (or Accept: application/geo+json). From the CLI, add --geojson out.geojson to geocode, elevation, query or report. A location report becomes a map layer: the place plus every nearby amenity, with distances.

Natural-language questions

result = engine.ask("How far is the Eiffel Tower from Big Ben?")
print(result.spatial_context.summary)
print(result.sources)  # the model, then each tool it called

ask uses a local Ollama model first and falls back to free OpenRouter models if OPENROUTER_API_KEY is set.

Agents

from geospark.agents import AgentCoordinator, GeoAgent, SiteSelector, SpatialReport

GeoAgent().run("Find all hospitals within 2km of the Eiffel Tower").findings["nearby"]

report = SpatialReport().analyze("Federation Square, Melbourne", radius_m=1000)
print(report.weather, report.accessibility, report.data_gaps)

best = SiteSelector().find(within="Zurich", near=["hospital", "school"], facility_type="pharmacy").best

AgentCoordinator().run("Find the best location for a cafe in Melbourne CBD near schools")

Agents use an LLM to read the goal. Without one, a rule-based parser still handles requests like the one above: multi-word place names, radii such as "2km", and amenity types.

Memory, context and flows

from geospark.memory import SpatialIntelligence

intel = SpatialIntelligence()
intel.remember_fact("Eiffel Tower is at 48.8584 N, 2.2945 E", source="user")
intel.remember_episode("PM2.5 was 120 in Delhi", importance=0.8, source="tool:air_quality")
intel.recall("Where is the Eiffel Tower?", limit=5)  # semantic with Ollama embeddings, keyword match without
intel.find_contradictions()
from geospark.flows import ChatFlowSession, FlowRunner, make_ollama_chat_fn

session = ChatFlowSession(llm_fn=make_ollama_chat_fn("qwen2.5:7b"))
result = session.run("Geocode Valencia, Spain then check its elevation")
if result.flow:
    FlowRunner().run(result.flow)  # a validated DAG, built one tool call at a time

Command line

geospark distance -37.8136 144.9631 -33.8688 151.2093   # Melbourne → Sydney: 713.86 km
geospark check contains '<geojson A>' '<geojson B>'
geospark geocode "Tokyo Tower, Japan"
geospark elevation -37.8177 144.9691
geospark ask "Is Tokyo closer to Seoul or Beijing?"

geospark agent "Find all parks within 2km of Big Ben"
geospark report "Federation Square, Melbourne"
geospark site-select --within "Paris" --near "school,hospital" --facility pharmacy
geospark multi-agent "Analyze Federation Square" --stream

geospark data weather "Melbourne, Australia"
geospark data air-quality "Delhi"        # needs OPENAQ_API_KEY
geospark data fires "Amazon Rainforest"  # needs FIRMS_MAP_KEY

geospark flow list
geospark flow build "Geocode Valencia then check its elevation" --run
geospark info                            # versions and optional extras

Benchmark results

GeoSpark Bench v2 ran 9 models under three protocols: the bare question, step-by-step reasoning (CoT), and the question with GeoSpark's MCP tools attached. The models were five open-weight ≤9B (Qwen 2.5 7B, Llama 3.1 8B, Gemma 2 9B, Mistral 7B, Phi-3.5 3.8B), gpt-oss 20B, and three frontier APIs (GPT-5.4, Claude Sonnet 4.5, Gemini 2.5 Pro). Intervals are Wilson 95%, and the per-call cost ledger (~$41) is checked in. The full analysis is in a manuscript currently under peer review.

Numeric distance (distance_absolute, n = 80 per model; correct = within 10% of the pyproj.Geod.inv() geodesic):

Model Tier Bare LLM Step-by-step + GeoSpark tools
Qwen 2.5 7B open ≤9B 5.0% 20.0% 76.2%
Llama 3.1 8B open ≤9B 5.0% 21.2% 7.5% ᵃ
Gemma 2 9B open ≤9B 37.5% ᵇ 6.2% —
Mistral 7B open ≤9B 2.5% 2.5% 0.0% ᵃ
Phi-3.5 3.8B open ≤9B 1.2% 6.2% —
gpt-oss 20B open mid 93.8% 93.8% 91.2%
GPT-5.4 frontier 95.0% 90.0% 95.0%
Claude Sonnet 4.5 frontier 73.8% 92.5% 96.2%
Gemini 2.5 Pro frontier 25.0% 55.0% 85.0%

ᵃ The tools were available but not used properly: Llama mixed up km and m in the coordinates it got back, and Mistral answered in plain text. ᵇ Gemma's correct answers are well-known long-distance pairs, which is recall of training data, not computation. — means not evaluated with tools.

What we learned

  • Tools are a leveller. The bare-LLM range spans 1–95%. With tools, every model that makes valid tool calls lands between 76% and 96%. Qwen gained 71 points; GPT-5.4 gained nothing, because it was already at 95%.
  • Aggregate scores hide bias. Qwen's 56% on topology comes from saying "yes": it scores 93% on intersects (where the data skews true) and 3% on disjoint. GPT-5.4 scores 0% on touches. With GeoSpark, every predicate is 100%, because the engine computes it.
  • Step-by-step prompting is not a universal fix. It helps some models and hurts others.
  • Knowledge questions don't need tools. "Did the Amazon lose forest between 2015 and 2023?" is answered well at every scale without them.

Re-run it yourself with python -m geospark.bench run. The datasets regenerate identically from python -m geospark.bench.generate_datasets; see the datasets README for provenance, and the benchmark report for every table.

Self-hosting

git clone https://github.com/Maz2580/geospark && cd geospark
cp .env.example .env     # set GEOSPARK_ENV=production and GEOSPARK_ADMIN_TOKEN
docker compose -f docker-compose.prod.yml up -d

The hosted API is designed to stay open without a key, with guard rails:

  • An admin token protects operator routes (audit, usage, flow storage). The LLM gateway needs it or a revocable partner key.
  • Public callers can only choose the local model or free models, so nobody can spend your paid API credit.
  • Each visitor gets a private, size-capped, auto-expiring sandbox for the memory and context demos.
  • Rate limits use the real client IP, and LLM work has a concurrency cap.
  • Inputs are bounded, and the audit log stores hashed IPs.

All state lives under GEOSPARK_DATA_DIR. To lift Nominatim's public limits, point GEOSPARK_NOMINATIM_URL at your own server. Details are in deploy/DEPLOY.md and .env.example.

Roadmap

  • v0.7: verifiable answers.
    • Every result carries its source, licence, timestamp, CRS and units.
    • A "cannot answer" response instead of a guess.
    • Data-quality flags, such as geocoder confidence and DEM accuracy.
    • GeoJSON FeatureCollections that load straight into QGIS.
    • The MCP server updated to the 2026 spec, with structured GeoJSON output and a hosted HTTP endpoint.
  • v0.8: more data, more analysis.
    • Overture Maps buildings, places and roads through DuckDB, with no key.
    • Real isochrones and travel-time matrices.
    • Bring your own GeoJSON, GeoPackage or GeoParquet.
    • Reproducible run manifests.
  • Ongoing: external benchmarks (GPSBench, GeoBenchX), a QGIS bridge, and more MCP tools.

Data attribution

GeoSpark computes geometry locally, but it fetches data from public services. Results built on them keep the providers' licences, so please credit them when you publish:

  • OpenStreetMap (geocoding, amenities, map tiles): © OpenStreetMap contributors, ODbL
  • Copernicus DEM GLO-30 (elevation): © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018, provided under COPERNICUS by the European Union and ESA
  • Open-Meteo (weather): CC BY 4.0
  • OpenAQ (air quality) and NASA FIRMS (active fires): see each provider's terms. Air-quality results include each station's licence.

Elevation results include an attribution field you can copy directly. The full list is in NOTICE.

Development

git clone https://github.com/Maz2580/geospark && cd geospark
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,api,mcp]"
pytest                       # 1003 tests; offline, never touch ~/.geospark
ruff check geospark tests

Contributions are welcome. See CONTRIBUTING.md, and report security issues privately as described in SECURITY.md. The architecture overview is in docs/ARCHITECTURE.md, and release notes are in CHANGELOG.md.

Citing

If GeoSpark or GeoSpark Bench helps your research, please cite it. GitHub's "Cite this repository" button uses CITATION.cff.

Author

Created by Mazdak Ghasemi Tootkaboni (University of Melbourne) · ORCID 0000-0001-8084-5270 · GitHub @Maz2580

License

Apache 2.0. Copyright 2024-2026 Mazdak Ghasemi Tootkaboni.

Metadata

Release files for geospark-ai 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for geospark-ai 0.7.0
File Size Uploaded Provenance
geospark_ai-0.7.0.tar.gz 423.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for geospark-ai 0.7.0
File Interpreter ABI Platform
geospark_ai-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 804.8 kB

Release files / geospark_ai-0.7.0.tar.gz

Download URL geospark_ai-0.7.0.tar.gz
Size 423.3 kB
Tags Source
SHA-256 checksum
How to use checksums
8d0b4bea472150e23401a1ae7356dc15168889f94a24c603b7afc7192d43872b
BLAKE2b-256 checksum
How to use checksums
a36aec027c10fcada2701638505dc739fa6049de4272290e8043d254718feb04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / geospark_ai-0.7.0-py3-none-any.whl

Download URL geospark_ai-0.7.0-py3-none-any.whl
Size 381.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8f4198170e3fff15a8585be07f88df75cf3cb763151b3ba8e084e21dbd5ca19b
BLAKE2b-256 checksum
How to use checksums
11f934acaf188d2bd5b30ef85504270bebaf653ae62c50d348d8d846aa30d423
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for geospark-ai 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for geospark_ai-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page