GeoSilo
Compact geometry storage for DuckDB.
On this page
Technical Overview
Geometry without paying the WKB tax
DuckDB's spatial extension stores geometry as WKB β float64 coordinate pairs at 16 bytes each, even though adjacent vertices in a polygon share nearly all their bits. GeoSilo replaces that with a delta-encoded integer layout and a first-class GEOSILO column type. Most existing SQL keeps working because GeoSilo overlays the same ST_* surface; the introspection / bounding-box / measurement calls get native overloads that skip the WKB decode entirely.
What this extension is for
Compact geometry storage with a transparent ST_* surface. The GEOSILO type is a drop-in replacement for GEOMETRY columns in the cases GeoSilo targets β large static or slowly-changing polygon / point datasets where on-disk size and bounding-box / area / length speed matter more than complex spatial operations.
-
β’
Smaller on disk: On the TIGER/Line 2025 US Census dataset,
GEOSILO + ZSTDis ~21% smaller than DuckDB'sGEOMETRYcolumn (and ~70% smaller as raw blobs vs WKB). The win comes from delta-encoded int16 deltas compressing exceptionally well underZSTD. -
β’
Native ST_* fast paths:
ST_GeometryType,ST_IsEmpty,ST_NPoints,ST_X/ST_Y,ST_XMin/ST_XMax/ST_YMin/ST_YMax,ST_Area,ST_Length,ST_Perimeterall have GEOSILO-native implementations. They read the silo header or walk the integer delta stream directly β no GEOS round-trip. -
β’
Transparent fallback: Any
ST_*function without a native GEOSILO overload βST_Buffer,ST_Union,ST_AsText, etc. β still works. The implicitGEOSILO β GEOMETRYcast runsgeosilo_decodeon demand. -
β’
Arrow IPC interchange: GeoSilo registers an Arrow extension type named
queryfarm.geosilo. Producers tag a binary column with that metadata; the DuckDB consumer auto-decodes silo blobs toGEOMETRYon read and re-encodes on write. CRS rides through the metadata so scale stays consistent end-to-end.
How the encoding works
GeoSilo's encoding is straightforward and verifiable from the README. No ML, no learned codec β just delta encoding plus integer scaling.
-
β’
Coordinates as scaled integers: Each
(x, y)pair is multiplied by an integer scale and stored as integers. ForEPSG:4326(degrees), the default scale is1e7β about 1 cm precision. For projected CRSes like UTM orEPSG:3857(meters), the default is100, also 1 cm. - β’ Delta encoding within rings: The first vertex of each ring is stored as a 4-byte int32. Every subsequent vertex is a 2-byte int16 delta from the previous one. Roughly 90% of deltas fit in 2 bytes; the remainder fall back to a 6-byte escape sequence.
-
β’
Tight headers, no closing vertex: Points use a 1-byte compact header. Polygon rings omit the closing duplicate vertex (it's reconstructed on decode). The header carries
geometry_type,vertex_type, andscale, all readable bygeosilo_metadatawithout touching the body. -
β’
Bounding-box and area on integers: Native
ST_XMin/XMax/YMin/YMaxwalk the int16 delta stream and accumulate; nativeST_Arearuns the shoelace formula on int64 coordinates rebuilt from the same stream. Both avoid the GEOS dependency surface for these calls.
Honest scope
GeoSilo is a storage and fast-path extension, not a replacement for GEOS-backed spatial operations.
-
β’
Native overloads are limited: The native fast path covers introspection, bounding-box accessors, and area / length / perimeter. Operations like
ST_Buffer,ST_Union,ST_Intersection,ST_AsText, and most of the widerST_*surface still auto-decode toGEOMETRYand usespatial's GEOS-backed implementation. -
β’
Lossy at the chosen scale: Coordinates round to the integer scale you pick. The defaults give ~1 cm precision for the common CRSes; finer-grained data needs an explicit
geosilo_encode(geom, scale). Round-tripping through GeoSilo and back is not bit-exact with the original float64 WKB. -
β’
ZSTD does the heavy lifting: Without
ZSTD, raw GEOSILO blobs are actually larger than DuckDB's columnarGEOMETRYstorage.USING COMPRESSION zstdis what makes the storage win real β use it. -
β’
Spatial extension required: GeoSilo overlays
spatial'sST_*namespace. Loadspatialfirst, thengeosilo, in every connection that needs the GEOSILO type.
Common Use Cases
Shrink a static polygon dataset
TIGER/Line, OSM extracts, parcels, admin boundaries β geometry that's read often and rewritten rarely. Define the column as GEOSILO USING COMPRESSION zstd and the same SQL keeps working at a fraction of the storage cost.
Native bounding-box prefilter
Filter on ST_XMin / ST_XMax / ST_YMin / ST_YMax before any GEOS-backed operation. The native overloads run on integer deltas β fast enough that the prefilter pays for itself even in single-table scans.
Compact geometry over the wire
Use the queryfarm.geosilo Arrow extension type to ship encoded geometry between services. Smaller payloads, transparent decode at the DuckDB consumer.
Fast measurement scans
Per-row ST_Area / ST_Length / ST_Perimeter on millions of polygons without the WKB decode loop. The shoelace runs on int64; ZSTD keeps the data small enough that I/O isn't the bottleneck either.
Deep Dive
Technical Details
What you can do with one column type
The single most useful pattern: take an existing GEOMETRY column and re-declare it as GEOSILO with ZSTD compression. Every existing ST_* query still works β and the introspection / bounding-box / measurement calls get faster:
INSTALL spatial; LOAD spatial;INSTALL geosilo FROM community; LOAD geosilo;
-- Compact geometry, transparent ST_* surfaceCREATE TABLE parcels ( id INTEGER, name VARCHAR, geom GEOSILO('EPSG:4326') USING COMPRESSION zstd);
INSERT INTO parcels SELECT id, name, geom FROM raw_parcels;
-- Bounding-box prefilter β entirely native, no WKB decodeSELECT id, ST_Area(geom)FROM parcelsWHERE ST_XMin(geom) > -78 AND ST_XMax(geom) < -76;The GEOSILO type delta-encodes coordinates as scaled integers β each subsequent vertex of a ring is a 2-byte int16 offset from the previous one. On the TIGER/Line 2025 US Census dataset, GEOSILO + ZSTD is ~21% smaller than DuckDBβs columnar GEOMETRY storage.
GeoSilo is a compact storage format with a handful of native ST_* overloads. It is not a re-implementation of GEOS. The native fast path covers exactly: ST_GeometryType, ST_IsEmpty, ST_NPoints, ST_X / ST_Y, ST_XMin / ST_XMax / ST_YMin / ST_YMax, ST_Area, ST_Length, ST_Perimeter. Everything else β ST_Buffer, ST_Union, ST_Intersection, ST_AsText, and the rest of the spatial extension surface β works via the implicit GEOSILO β GEOMETRY cast (which runs geosilo_decode under the hood).
The encoding is also lossy at the chosen integer scale. Defaults give ~1 cm precision; finer data needs an explicit geosilo_encode(geom, scale).
For unbounded GEOS-backed analytics, use spatial directly. For storage-heavy workflows where bounding-box filtering and area / length scans dominate, GeoSilo is the right tool.
Setup
GeoSilo overlays spatialβs ST_* namespace, so load both β spatial first:
INSTALL spatial; LOAD spatial;INSTALL geosilo FROM community; LOAD geosilo;The GEOSILO column type
Declare a column as GEOSILO, optionally tagging the CRS. With a CRS the integer scale is auto-detected (degrees β 1e7, meters β 100); without one, the default is 1e7:
-- With CRS β scale auto-detectedCREATE TABLE parcels ( id INTEGER, name VARCHAR, geom GEOSILO('EPSG:4326') USING COMPRESSION zstd);-- Without CRS β default scale 1e7CREATE TABLE shapes (geom GEOSILO USING COMPRESSION zstd);USING COMPRESSION zstd is the recommended setting β itβs where the storage win comes from. See CREATE TABLE for the full DDL.
The implicit GEOMETRY β GEOSILO cast auto-encodes on insert, so you can populate from any GEOMETRY source β Parquet, CSV-then-ST_GeomFromText, another DuckDB table:
INSERT INTO parcels SELECT id, geom FROM raw_data;Whatβs native, what auto-decodes
Standard spatial functions all work on GEOSILO columns. The ones below have native GEOSILO overloads that read the silo header or walk the integer delta stream β no WKB round-trip:
| Group | Native overloads |
|---|---|
| Introspection | ST_GeometryType, ST_IsEmpty, ST_NPoints, ST_X, ST_Y |
| Bounding box | ST_XMin, ST_XMax, ST_YMin, ST_YMax |
| Measurement | ST_Area, ST_Length, ST_Perimeter |
Everything else (ST_Buffer, ST_Union, ST_Intersection, ST_AsText, ST_Centroid, etc.) auto-decodes via the implicit cast and runs through the GEOS-backed spatial implementation.
Encoding format, briefly
The format is verifiable and small enough to summarize:
- Coordinates scale to integers (
-75.509491 Γ 1e7 = -755094910). - The first vertex of each ring is a 4-byte int32 absolute coordinate.
- Subsequent vertices are stored as 2-byte int16 deltas from the previous; ~90% fit. The rest use a 6-byte escape sequence.
- Polygon rings omit the closing duplicate vertex β itβs reconstructed on decode.
- Points use a 1-byte compact header.
The header (geometry_type, vertex_type, scale) is readable without decoding the body β see geosilo_metadata.
Scale and precision
| CRS family | Units | Default scale | Precision |
|---|---|---|---|
EPSG:4326 / NAD83 |
degrees | 10,000,000 |
~1 cm |
UTM / EPSG:3857 |
meters | 100 |
1 cm |
Auto-applied when you declare GEOSILO('EPSG:...'). Override per-call with the two-argument form:
SELECT geosilo_encode(geom, 1000) FROM millimeter_data;Arrow IPC transport
GeoSilo registers an Arrow extension type named queryfarm.geosilo. A producer tags an Arrow binary field with the right metadata and the DuckDB consumer (with GeoSilo loaded) auto-decodes silo blobs to GEOMETRY on read and re-encodes on write β CRS rides through the fieldβs metadata:
geom_field = pa.field("geom", pa.binary(), metadata={ b"ARROW:extension:name": b"queryfarm.geosilo", b"ARROW:extension:metadata": b'{"crs":"EPSG:4326"}',})Sibling extensions in the geo bucket
a5β pentagonal global geospatial index for spatial aggregation, joins, and equal-area binning. Pairs well: index witha5, store the underlying geometry withgeosilo.lindelβ Hilbert / Morton space-filling curves for ordering multi-dimensional data on disk. Sort aGEOSILOtable by alindel-encoded centroid to keep nearby geometries on the same Parquet row group / DuckDB block.
Install
INSTALL geosilo FROM community;
LOAD geosilo;
Quick Start
A GEOSILO column with ZSTD β create, load from a GEOMETRY source, query with ST_*
-- A GEOSILO column with ZSTD β the recommended setup
CREATE TABLE parcels (
id INTEGER,
name VARCHAR,
geom GEOSILO('EPSG:4326') USING COMPRESSION zstd
);
-- Insert from any GEOMETRY source β implicit cast auto-encodes
INSERT INTO parcels SELECT id, name, geom FROM raw_parcels;
-- ST_* runs directly on the GEOSILO column; native overloads skip the decode
SELECT id, ST_Area(geom), ST_X(ST_Centroid(geom)) FROM parcels;
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
Encoding
Encode/decode between |
||
| geosilo_decode() |
Decode a GEOSILO blob back into a standard GEOMETRY.
|
|
| geosilo_encode() |
Encode a GEOMETRY into the compact GEOSILO format and return a BLOB.
|
|
| geosilo_metadata() | Read a GEOSILO blob's header without decoding the geometry. | |
|
Introspection (native)
ST_* overloads with native GEOSILO implementations β type, emptiness, point count, X/Y, and the four bounding-box accessors. They read the silo header / delta stream directly without round-tripping through WKB, which is where the speed-up on bounding-box prefilters and per-row coordinate access comes from. |
||
| ST_GeometryType() | Native GEOSILO overload β returns the geometry type (e.g. | |
| ST_IsEmpty() |
Native GEOSILO overload β TRUE if the encoded geometry has no vertices.
|
|
| ST_NPoints() | Native GEOSILO overload β total vertex count, read from the silo header. | |
| ST_X() |
Native GEOSILO overload β X coordinate of a single-point geometry, decoded from the silo header without materializing a GEOMETRY.
|
|
| ST_XMax() | Native GEOSILO overload β maximum X across the geometry. | |
| ST_XMin() | Native GEOSILO overload β minimum X across the geometry, computed by walking the integer delta stream. | |
| ST_Y() |
Native GEOSILO overload β Y coordinate of a single-point geometry, decoded from the silo header without materializing a GEOMETRY.
|
|
| ST_YMax() | Native GEOSILO overload β maximum Y across the geometry. | |
| ST_YMin() | Native GEOSILO overload β minimum Y across the geometry. | |
|
Measurement (native)
ST_* overloads for area, length, and perimeter computed directly on the integer delta stream (shoelace on int64 for area). No WKB decode, no GEOS dependency surface for these calls. |
||
| ST_Area() | Native GEOSILO overload β polygon area computed via the shoelace formula directly on int64 coordinates rebuilt from the delta stream. | |
| ST_Length() |
Native GEOSILO overload β total length of linear geometries, computed on the integer delta stream without rehydrating a GEOMETRY.
|
|
| ST_Perimeter() |
Native GEOSILO overload β perimeter of polygonal geometries, computed on the integer delta stream without rehydrating a GEOMETRY.
|
|
|
Types
|
||
| GEOSILO | Logical type registered by this extension. | |
No extension contents match that search.
API Reference
Function Documentation
Data model
Registered Types
Logical types this extension adds to DuckDB. Functions in the reference may accept or return these names directly.
-
GEOSILOLogical type registered by this extension.
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
Cookbook setup
Load spatial first, then geosilo:
INSTALL spatial; LOAD spatial;INSTALL geosilo FROM community; LOAD geosilo;The GEOSILO column type and the native ST_* overloads only register once both are loaded β geosilo overlays spatialβs ST_* namespace.
Define a compact geometry table
The recommended shape β explicit CRS, ZSTD compression:
CREATE TABLE parcels ( id INTEGER, name VARCHAR, geom GEOSILO('EPSG:4326') USING COMPRESSION zstd);The CRS lets GeoSilo pick a sensible integer scale automatically (1e7 for degrees, 100 for projected meters). USING COMPRESSION zstd is where the storage win comes from β without it, raw GEOSILO blobs are larger than DuckDBβs columnar GEOMETRY storage. See CREATE TABLE for the DDL surface.
Load from any GEOMETRY source
The implicit GEOMETRY β GEOSILO cast auto-encodes on insert:
-- From another DuckDB tableINSERT INTO parcelsSELECT id, name, geom FROM raw_parcels;-- From WKT in CSVINSERT INTO parcelsSELECT id, name, ST_GeomFromText(wkt) FROM read_csv('parcels.csv');-- From Parquet with a WKB columnINSERT INTO parcelsSELECT id, name, ST_GeomFromWKB(geom_wkb)FROM read_parquet('parcels.parquet');Sources can be any expression returning GEOMETRY β see spatialβs constructor functions for the full list.
Native bounding-box prefilter
The four ST_XMin / ST_XMax / ST_YMin / ST_YMax calls have GEOSILO-native implementations β they walk the integer delta stream without rebuilding a GEOMETRY. Use them in WHERE clauses to prefilter before any GEOS-backed work:
-- Everything in a longitude band β entirely nativeSELECT id, nameFROM parcelsWHERE ST_XMin(geom) > -78 AND ST_XMax(geom) < -76;-- Hand off to GEOS only for the survivors of the prefilterSELECT id, ST_AsText(ST_Centroid(geom))FROM parcelsWHERE ST_XMin(geom) > -78 AND ST_XMax(geom) < -76 AND ST_YMin(geom) > 39 AND ST_YMax(geom) < 41;See ST_XMin / ST_XMax / ST_YMin / ST_YMax.
Native area, length, perimeter
ST_Area on a GEOSILO column runs the shoelace formula on int64 coordinates rebuilt from the delta stream β no WKB decode:
-- Per-feature areaSELECT id, ST_Area(geom) AS areaFROM parcelsORDER BY area DESCLIMIT 10;-- Total length of a road networkSELECT SUM(ST_Length(geom)) AS total_road_lengthFROM roads;-- Perimeter histogramSELECT FLOOR(LOG(ST_Perimeter(geom))) AS bucket, COUNT(*) AS nFROM parcelsGROUP BY bucketORDER BY bucket;See ST_Area, ST_Length, ST_Perimeter.
Per-point coordinate access
ST_X / ST_Y on point geometries decode straight from the silo header:
SELECT id, ST_X(geom) AS lon, ST_Y(geom) AS latFROM stations;For non-point geometries, use ST_Centroid (which auto-decodes to GEOMETRY) and then ST_X / ST_Y on the resulting point.
Filter by geometry type without decoding
geosilo_metadata reads the silo header directly. Combine with ST_GeometryType (which is also native) to filter cheaply by type:
-- Counts by geometry type β no WKB decode per rowSELECT ST_GeometryType(geom) AS gtype, COUNT(*) AS nFROM mixed_geom_tableGROUP BY gtypeORDER BY n DESC;-- Inspect the integer scale of stored blobsSELECT geosilo_metadata(silo_blob) AS metaFROM compact_tableLIMIT 5;-- {'geometry_type':'POLYGON','vertex_type':'XY','scale':10000000}Manual encode / decode for BLOB pipelines
When you control the BLOB pipeline directly β exporting to a file format, shipping over a non-Arrow protocol, staging β use geosilo_encode and geosilo_decode:
-- Encode for exportCOPY ( SELECT id, geosilo_encode(geom) AS silo FROM parcels_geometry) TO 'parcels_silo.parquet' (FORMAT 'parquet');
-- Decode on readSELECT id, ST_AsText(geosilo_decode(silo)) AS wktFROM read_parquet('parcels_silo.parquet')LIMIT 5;For a custom integer scale (e.g. millimeter-precision projected data), pass the second argument:
SELECT geosilo_encode(geom, 1000) FROM mm_precision_data;Mix native fast paths with GEOS-backed operations
Anything outside the native overload set auto-decodes through the implicit GEOSILO β GEOMETRY cast. You donβt need to think about it β but if youβre scanning many rows, push the native operations first to shrink the candidate set:
-- ST_Buffer auto-decodes; ST_XMin/XMax/YMin/YMax run native firstSELECT id, ST_AsText(ST_Buffer(geom, 0.001)) AS buffered_wktFROM parcelsWHERE ST_XMin(geom) > -78 AND ST_XMax(geom) < -76LIMIT 100;See the spatial extension for the GEOS-backed surface that auto-decodes.
Pair with sibling geo extensions
GeoSilo handles compact storage; pair it with the other geo bucket extensions for indexing and ordering:
-- a5: pentagonal geospatial index for aggregation / joinsINSTALL a5 FROM community; LOAD a5;
-- Bin parcels by A5 cell at resolution 12SELECT a5_lonlat_to_cell(ST_X(ST_Centroid(geom)), ST_Y(ST_Centroid(geom)), 12) AS cell, COUNT(*) AS parcels_in_cellFROM parcelsGROUP BY cellORDER BY parcels_in_cell DESC;See a5 for the indexing surface, and lindel for space-filling curve ordering β sort a GEOSILO table by a Hilbert-encoded centroid to keep nearby geometries on the same DuckDB block / Parquet row group.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
- Linux x86_64 aarch64
- Linux (musl) Not available
- macOS Intel Apple Silicon
- Windows x86_64
- WASM eh mvp threads
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | x86_64 | 3.26 MB |
| Linux | aarch64 | 2.89 MB |
| macOS | Intel | 2.64 MB |
| macOS | Apple Silicon | 2.30 MB |
| Windows | x86_64 | 7.40 MB |
| WASM | eh | 53.8 KB |
| WASM | mvp | 46.6 KB |
| WASM | threads | 54.0 KB |
Compressed download size from the Haybarn extension repository.
DuckDB & Haybarn
Release calendar- DuckDB v1.5.5 Haybarn 1.5.5-rc1 Supported