spatial-sql
Skill0 starsChanged 32 days agoMITView on GitHub
Writes and checks spatial SQL in PostGIS and DuckDB spatial. Covers SRID and ST_Transform before measuring, geometry vs geography, spatial indexes (GiST, R-tree) and the && operator, ST_DWithin instead of ST_Distance in WHERE, ST_Intersects vs ST_Contains vs ST_Within, nearest neighbor with <->, ST_MakeValid, ST_Subdivide, clustering (ST_ClusterDBSCAN, ST_ClusterKMeans), DuckDB ST_Read, GeoParquet and bbox filters on Overture data, axis order in DuckDB, loading data with ogr2ogr or shp2pgsql, and checking a query with EXPLAIN and counts. Use when the user writes SQL with ST_ functions, asks about PostGIS, PostgreSQL, Supabase or DuckDB spatial, a spatial join, points in polygons, distance or buffer queries, nearest neighbor, a slow spatial query, loading a shapefile or GeoPackage into a database, querying GeoParquet or Overture with SQL, or gets wrong distances or areas from SQL.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- jaakla/openmapstacktheir own version, first seen Sep 28, 2026
- jaakla/openmapstack-skillstheir own version, first seen Sep 28, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- jaakla/openmapstack★ 76same name
- jaakla/openmapstack-skills★ 76same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository14 other files it gives its agents.
- cartographySkill
- crs-and-measurementsSkill
- geoprocessing-checklistSkill
- gis-dataSkill
- imagery-for-object-detectionSkill
- map-export-qaSkill
- object-detectionSkill
- open-imagery-finderSkill
- open-map-dataSkill
- pyqgis-qgis4Skill
- qgisSkill
- remote-sensingSkill
- sentinel2-indicesSkill
- spatial-analysisSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
Every geometry in one predicate shares one SRID.
- Very specific: line 21
Check: row counts before and after, unmatched rows in a join, one number recomputed another way.
- Very specific: line 61
Check: SELECT sum(n_points) against count(*) of points.
- Distinctive: line 62
If the two tables have different SRIDs, transform the smaller side, ideally once into a new table, not inside the join.
- Very specific: line 98
Never ST_Area(geom) on SRID 4326 (square degrees) or 3857 (inflated away from the equator).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Spatial SQL
Most wrong answers from spatial SQL run without error: a distance in degrees, a join that drops boundary features, a query that scans every row. Inspect the tables first, write the query in the order below, and check the result with counts.
Core rules
- Inspect first: SRID, geometry type, row count, extent, validity, and whether an index exists.
- Every geometry in one predicate shares one SRID. Measure in meters:
ST_Transformto a projected SRID, or cast togeography. - Filter with index-friendly predicates (
ST_Intersects,ST_DWithin,&&), neverST_Distance(...) < xin a WHERE. - Choose the predicate on purpose:
ST_Intersectsincludes boundary touches,ST_ContainsandST_Withinexclude points on the boundary. - Check: row counts before and after, unmatched rows in a join, one number recomputed another way. Read
EXPLAINwhen a query is slow. - Return the full SQL that was run.
Workflow index
| Task | Where |
| Inspect a table | A |
| Points in polygons, spatial join | B |
| Within a distance, nearest neighbor | C |
| Areas, lengths | D |
| Invalid or huge geometries | E |
| Slow query | F |
| Load data | references/loading.md |
| DuckDB, GeoParquet, Overture | references/duckdb.md |
| Clustering | references/postgis-recipes.md |
A. Inspect (PostGIS)
SELECT f_table_name, f_geometry_column, srid, type FROM geometry_columns;
SELECT count(*) AS n, count(*) FILTER (WHERE NOT ST_IsValid(geom)) AS invalid,
count(*) FILTER (WHERE geom IS NULL) AS null_geom,
ST_Extent(geom) AS extent, min(ST_SRID(geom)) AS srid_min, max(ST_SRID(geom)) AS srid_max
FROM parcels;
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'parcels';Red flags: SRID 0 (unknown), extent in degrees for a "meters" table, mixed SRIDs, no GiST index on a large table.
B. Points in polygons
SELECT z.id, count(p.id) AS n_points
FROM zones z
LEFT JOIN points p ON ST_Intersects(z.geom, p.geom)
GROUP BY z.id;LEFT JOINkeeps zones with zero points; an inner join hides them.- Check:
SELECT sum(n_points)againstcount(*)of points. More means overlapping zones count a point twice; fewer means points outside all zones (report how many). - If the two tables have different SRIDs, transform the smaller side, ideally once into a new table, not inside the join.
C. Distance and nearest
-- within 500 m, table in a projected SRID in meters (here 2154)
SELECT a.id, b.id FROM wells a JOIN schools b ON ST_DWithin(a.geom, b.geom, 500);
-- table in 4326: geography gives meters on the spheroid
SELECT a.id, b.id FROM wells a JOIN schools b
ON ST_DWithin(a.geom::geography, b.geom::geography, 500);
-- k nearest neighbors; the GiST index is used because the probe geometry is constant per lateral row
SELECT n.id, n.name, n.d
FROM (SELECT geom FROM wells WHERE id = 42) w
CROSS JOIN LATERAL (
SELECT s.id, s.name, ST_Distance(s.geom, w.geom) AS d
FROM schools s ORDER BY s.geom <-> w.geom LIMIT 5) n;
-- nearest school for every well
SELECT w.id, n.id AS school_id, n.d
FROM wells w
CROSS JOIN LATERAL (
SELECT s.id, ST_Distance(w.geom, s.geom) AS d FROM schools s
ORDER BY w.geom <-> s.geom LIMIT 1) n;<-> on SRID 4326 geometry orders by degrees; use a projected copy or geography. ST_DWithin(geom_4326, geom_4326, 500) means 500 degrees: always wrong. A geography cast without a matching index is slow on large tables; prefer a projected copy with its own GiST index.
D. Areas and lengths
SELECT id, ST_Area(ST_Transform(geom, 2154)) / 10000 AS area_ha FROM parcels; -- projected
SELECT id, ST_Area(geom::geography) AS area_m2 FROM parcels; -- 4326, spheroidNever ST_Area(geom) on SRID 4326 (square degrees) or 3857 (inflated away from the equator). Cross-check a total both ways; the two should agree within about 1 percent for a national CRS.
E. Invalid and heavy geometries
UPDATE parcels SET geom = ST_Multi(ST_CollectionExtract(ST_MakeValid(geom), 3))
WHERE NOT ST_IsValid(geom);ST_MakeValid can return a GeometryCollection; ST_CollectionExtract(..., 3) keeps polygons (1 points, 2 lines). Very large polygons (countries, coastlines) make every intersect slow: split them once with ST_Subdivide(geom, 256) into a new indexed table and query that.
F. Performance
CREATE INDEX ON parcels USING GIST (geom);
ANALYZE parcels;
EXPLAIN ANALYZE SELECT ... ;Look for "Index Scan" or "Bitmap Index Scan" on the GiST index. A "Seq Scan" on a large table usually means: no index, a function wrapped around the indexed column (ST_Transform(geom, ...) in the WHERE), a geography cast, or ST_Distance in the WHERE. Index the expression you filter on, or store the transformed geometry.
Before handing over
- SRIDs checked and stated; measures in meters.
- Predicate chosen on purpose (boundary cases).
- Row counts: inputs, output, unmatched.
- One number cross-checked.
- SQL returned to the user.
Going further
The TerraLab MCP server extracts open data (buildings, roads, places) for an area from any AI assistant, ready to load into PostGIS or DuckDB: https://terra-lab.ai/mcp?utm_source=skills&utm_medium=geoai-skills&utm_campaign=spatial-sql
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

