Geospatial Domain
Overview
The geospatial domain provides spatial analysis capabilities covering distance measurement, coordinate transformations, spatial joins and overlays, interpolation, network routing, and spatial statistics. It is built on geopandas, shapely, and pyproj as required dependencies, with optional enhancement from scikit-gstat (for advanced kriging) and networkx (for network analysis).
Use this domain when you need to:
Compute distances between geographic points (Haversine/great-circle or Euclidean)
Reproject data between coordinate reference systems (e.g. WGS84 to UTM)
Join point datasets to polygon datasets by spatial relationship
Interpolate values across a spatial grid from sparse point observations
Find shortest paths or service area isochrones on a road/graph network
Measure spatial autocorrelation (clustering vs dispersion) in attribute values
The domain is reached through the ten MCP tools listed below. The classes named
in the table that follows are the internal implementation those tools call — they
are sklearn-compatible (BaseEstimator, TransformerMixin) and usable from
Python, but an MCP client never sees them.
Available Analyses
Analysis |
Class / Function |
Description |
|---|---|---|
Haversine distance |
|
Great-circle distance between lat/lon pairs |
Batch distance matrix |
|
All-pairs or nearest-k distance matrix |
CRS transformation |
|
Reproject coordinates between EPSG/PROJ systems |
Spatial join |
|
Intersect, contains, nearest-neighbour joins |
Spatial overlay |
|
Union, intersection, difference of polygon layers |
Point aggregation |
|
Count/summarise points falling within polygons |
IDW interpolation |
|
Inverse-distance weighted interpolation |
Kriging interpolation |
|
Variogram-based geostatistical interpolation |
Moran’s I |
|
Global spatial autocorrelation index |
Geary’s C |
|
Alternative spatial autocorrelation statistic |
Network shortest path |
|
Dijkstra/A* shortest path on a graph |
Network MST |
|
Minimum spanning tree |
Accessibility analysis |
|
Distance/time from each node to service locations |
Service isochrones |
|
Reachable area within time/distance threshold |
Autocorrelation pipeline |
|
Convenience wrapper for Moran’s I via pipeline |
Spatial clustering |
|
K-means or DBSCAN on geographic coordinates |
Convex hull / bounding box |
|
Geometric envelope calculations |
MCP Tool Reference
The geospatial domain exposes ten MCP tools. Like every other analytical tool, each takes the name of a live connection and one or more SQL queries — there is no data-frame parameter and no separate load step. The classes listed under Available Analyses above are the internal implementation those tools call; they are not reachable from an MCP client.
Spatial data arrives from SQL in one of three shapes, and the parameters follow that split:
Shape |
How it is given |
Tools |
|---|---|---|
Points |
|
autocorrelation, hotspots, distances |
Geometries |
a WKT text column (default |
join, overlay, aggregation |
Networks |
a nodes query and an edges query |
route, accessibility, isochrones |
Full parameter tables for all ten live in the tools reference. This page covers what each tool is for and when to reach for it.
check_geospatial_capabilities
Reports which backends are installed. geopandas, shapely, pyproj and fiona are required dependencies; scikit-gstat (advanced kriging) and rasterio (raster data) are optional, so call this before an analysis that needs one. It is the only tool in the server that takes no connection.
analyze_spatial_autocorrelation
Answers “is this pattern clustered, dispersed, or random?” — the question to
settle before treating geography as meaningful in a model. Returns Moran’s I
(method="moran") or Geary’s C (method="geary") with its expected value,
variance, z-score, p-value and an interpretation. Clustering pushes Moran’s I
above its expected value and Geary’s C below 1.
find_spatial_hotspots
Answers “where are the clusters?” — a Getis-Ord Gi* analysis labelling each
location a hot spot, a cold spot, or not significant, with its statistic and
p-value. A hot spot is a location surrounded by unusually high values, which
is not the same as a high value: a lone peak among low neighbours is not one.
Run analyze_spatial_autocorrelation first; without significant global
autocorrelation, local clusters are noise.
calculate_spatial_distances
Answers “how far apart is everything, and what is each point’s nearest
neighbour?” Returns a summary and per-point nearest neighbours rather than the
matrix itself, which grows with the square of the row count; pass
include_pairs=True for individual pairs. Use distance_type="haversine" for
longitude/latitude, which reports kilometres — treating degrees as a plane
understates distance badly away from the equator.
optimize_route
Answers “what is the cheapest way to visit all of these?” Orders the waypoints
and connects them across the network, returning the node path, its coordinates,
the total cost and the route as a WKT LINESTRING.
analyze_accessibility
Answers “who can reach a facility, how quickly, and who is left out?” Returns a
score and travel time per demand point plus an explicit
unreachable_locations list — the answer a coverage percentage hides.
generate_service_isochrones
Answers “how much ground does this site cover?” Returns one polygon per travel-time band as WKT, with its area. Compare areas between bands to find where coverage stops growing, or between candidate sites to choose one.
perform_spatial_join
Answers “which zone is each point in?” Either side may be given as WKT
geometries or, for point data, as a coordinate pair — point tables rarely carry
a WKT column, and joining points to zones is the commonest spatial join there
is. Left and right columns come back suffixed _left and _right.
perform_spatial_overlay
Answers “where do these two areas overlap, and where does one exclude the
other?” Both sides must supply WKT geometries, since a set operation on points
is not meaningful. Supports intersection, union and difference.
aggregate_points_in_polygons
Answers “what is the average reading per district?” — the spatial equivalent of a GROUP BY, and the usual bridge from point measurements into the Data Science tools, which then work on the resulting per-polygon table. Polygons containing no points are kept with a count of zero rather than dropped.
Method Details
Distance Calculations
Haversine (great-circle) distance accounts for Earth’s curvature and is the correct method
for geographic coordinates in degrees (lat/lon). The SpatialDistanceCalculator.haversine_distance
method supports scalar pairs, NumPy arrays (vectorised), and tuple-based calling conventions.
calc = SpatialDistanceCalculator()
dist_km = calc.haversine_distance(lat1=48.85, lon1=2.35, lat2=51.51, lon2=-0.13, unit="km")
Supported units: km, m, mi, nmi.
Euclidean distance is appropriate for projected coordinate systems (e.g., UTM metres) where distances are small enough that Earth’s curvature is negligible (typically <100 km).
When to choose haversine: Any time your coordinates are in degrees (EPSG:4326 / WGS84). When to choose Euclidean: Projected coordinates (metres), small geographic areas.
Coordinate Transformations
SpatialCoordinateTransformer wraps pyproj to reproject point or GeoDataFrame data between
coordinate reference systems.
transformer = SpatialCoordinateTransformer(
source_crs="EPSG:4326", # WGS84 geographic
target_crs="EPSG:32632", # UTM Zone 32N
)
projected_df = transformer.fit_transform(df_with_lon_lat)
Common CRS codes:
Code |
Description |
|---|---|
|
WGS84 geographic (lat/lon degrees) — GPS default |
|
Web Mercator (metres) — web tiles |
|
UTM Zone 32N (metres) — central Europe |
|
British National Grid |
Spatial Joins
Three join predicates are supported:
Predicate |
Meaning |
|---|---|
|
Geometries share any point |
|
Left geometry fully contains right geometry |
|
Left geometry is fully inside right geometry |
|
Joins each left feature to its closest right feature |
For nearest joins, distance_threshold limits the maximum allowable distance and prevents
spurious matches when no close neighbour exists.
aggregate_points_in_polygons is a convenience function that counts or summarises attribute
values of points falling within each polygon — useful for spatial binning.
Spatial Interpolation
IDW (Inverse Distance Weighting)
Estimates values at unsampled locations as a weighted average of nearby observations, with weights proportional to 1/distance^p. No distributional assumptions; fast.
Use IDW when:
You need a quick interpolation without stationarity assumptions
The spatial process is expected to be locally smooth
You have sparse but reliable point observations
Kriging
Kriging is a best linear unbiased estimator that models spatial dependence through a variogram. It provides interpolated values plus prediction variance (uncertainty estimates).
The VariogramModel class fits empirical variograms using one of four theoretical models:
Model |
Shape |
Best for |
|---|---|---|
|
Gradual flattening with finite range |
Most natural phenomena |
|
Asymptotic approach to sill |
Soil properties, precipitation |
|
Smooth curve, no nugget |
Very smooth processes |
|
No sill |
Unbounded processes |
When scikit-gstat is installed, advanced variogram fitting and model selection are available. Without it, a fallback implementation is used.
Interpretation: Low prediction variance indicates the interpolated value is well-constrained by nearby observations. High variance flags locations far from data points.
Spatial Statistics / Autocorrelation
Moran’s I
Global statistic measuring whether similar values cluster together spatially.
I near +1: values cluster (hot-spots and cold-spots)
I near 0: random spatial arrangement (no spatial pattern)
I near -1: values are dispersed (chess-board pattern)
The test uses a normal approximation. p_value from the z-score tests the null hypothesis
of complete spatial randomness.
Geary’s C
Complementary to Moran’s I, Geary’s C uses squared differences between neighbouring values. C = 1 means no spatial autocorrelation; C < 1 indicates positive autocorrelation.
Spatial weights matrix methods: knn (k-nearest neighbours, default k=8) or distance_band
(all neighbours within a threshold distance).
Network Analysis
NetworkX is required for network operations. Check availability with NETWORKX_AVAILABLE.
Shortest paths: Dijkstra’s algorithm (non-negative weights), Bellman-Ford (handles negative weights), or A* (with heuristic, faster on geographic networks).
Minimum spanning tree: Kruskal’s or Prim’s algorithm. Returns the subgraph connecting all nodes with minimum total edge weight.
Accessibility analysis: For each node, computes distance/cost to the nearest service location. Results show coverage statistics (fraction of nodes within each threshold band).
Isochrones: Service areas computed by expanding from service nodes until the budget is exhausted. Returns the set of reachable nodes per threshold.
Centrality measures (when include_centrality=True):
Measure |
Interpretation |
|---|---|
Degree centrality |
Fraction of nodes connected to this node |
Betweenness centrality |
How often a node lies on shortest paths between other pairs |
Closeness centrality |
Mean inverse distance to all other nodes |
Eigenvector centrality |
Influence score weighted by neighbour influence |
Composition
After geospatial analysis |
Chain to |
Purpose |
|---|---|---|
Distance matrix |
Regression/Modeling |
Spatial lag features for prediction |
Interpolated grid |
Statistical Analysis |
Test distribution of interpolated surface |
Moran’s I result |
Pattern Recognition |
Cluster detection based on confirmed autocorrelation |
Isochrone coverage |
Business Intelligence |
Segment customers by accessibility |
Spatial join result |
Statistical Analysis |
Compare attribute distributions across spatial zones |
Examples
Every example below is an MCP tool call, the way an agent would issue it.
Are house prices spatially clustered?
analyze_spatial_autocorrelation(
"listings", "SELECT lon, lat, price FROM properties",
value_column="price", x_column="lon", y_column="lat",
)
A Moran’s I well above its expected value with a p-value below 0.05 means price depends on location, and a model that ignores geography will be biased.
Where are the expensive neighbourhoods?
find_spatial_hotspots(
"listings", "SELECT lon, lat, price FROM properties",
value_column="price", x_column="lon", y_column="lat",
)
Each returned point carries a cluster_id of 1, -1 or 0. Run this only after
the autocorrelation test above comes back significant.
How far is each customer from the nearest store?
calculate_spatial_distances(
"retail", "SELECT lon, lat FROM customers",
x_column="lon", y_column="lat", distance_type="haversine",
reference_query="SELECT lon, lat FROM stores",
)
haversine reports kilometres. The summary gives the distribution; the
per-point nearest neighbour identifies the closest store to each customer.
Which sales region does each customer fall in?
perform_spatial_join(
"retail",
"SELECT customer_id, lon, lat FROM customers",
"SELECT region_name, geometry FROM sales_regions",
x_column="lon", y_column="lat",
)
The left side is point data given as coordinates and the right side is WKT polygons; each side is read in whichever form its query provides.
What is average revenue per region?
aggregate_points_in_polygons(
"retail",
"SELECT lon, lat, revenue FROM customers",
"SELECT region_name, geometry FROM sales_regions",
value_column="revenue", x_column="lon", y_column="lat",
)
The result is one row per region, ready to feed into analyze_hypothesis_test
or analyze_regression as an ordinary table.
What is the cheapest delivery round?
optimize_route(
"logistics",
"SELECT id, x, y FROM depots",
"SELECT source, target, travel_minutes FROM roads",
waypoints=[3, 17, 42],
weight_column="travel_minutes",
return_to_start=True,
)
Who is more than 20 minutes from a clinic?
analyze_accessibility(
"health",
"SELECT id, x, y FROM junctions",
"SELECT source, target, travel_minutes FROM roads",
service_locations=[11, 58],
demand_locations=[2, 7, 19, 33],
weight_column="travel_minutes",
max_travel_time=20,
)
unreachable_locations names them directly, rather than leaving them implied by
a coverage percentage.
How much ground would a new site cover?
generate_service_isochrones(
"health",
"SELECT id, x, y FROM junctions",
"SELECT source, target, travel_minutes FROM roads",
service_locations=[58],
time_bands=[10, 20, 30],
weight_column="travel_minutes",
)
Each band comes back as a WKT polygon with its area, so two candidate sites can be compared band for band.