Terrana
Zero-config spatial API server. Point it at a CSV, Parquet, or GeoJSON file containing lat/lon columns and immediately get a REST API with spatial queries and geometry operations — no database setup, no PostGIS, no infrastructure.
# → REST API running at http://localhost:8080
Installing Rust
Terrana is built with Rust. If you don't already have a toolchain, install one with rustup — the official Rust installer:
# macOS / Linux
|
On Windows, download and run rustup-init.exe (or
winget install Rustlang.Rustup). Then restart your shell and confirm it worked:
rustup installs cargo (the build tool / package manager) alongside the compiler.
That's all you need — Terrana bundles DuckDB, so there's no system DuckDB, PostGIS,
or other infrastructure to install. (On first run, DuckDB downloads its spatial
extension from the network and caches it locally.)
Install
Terrana is published on crates.io. Install the
terrana binary with:
Or build and install from a checkout of this repository:
Usage
Arguments
| Argument | Description |
|---|---|
<FILE> |
Path to CSV, Parquet, GeoJSON, or .duckdb file |
Options
| Option | Description | Default |
|---|---|---|
--lat <COLUMN> |
Latitude column name | auto-detected |
--lon <COLUMN> |
Longitude column name | auto-detected |
--table <TABLE> |
Table name (DuckDB files only) | — |
--port <PORT> |
HTTP port | 8080 |
--bind <ADDR> |
Bind address | 127.0.0.1 |
--watch |
Re-index when source file changes | off |
--disk |
Use on-disk DuckDB storage (reduces RAM for large files) | off |
Auto-detection of lat/lon columns
When --lat / --lon are omitted, column names are scanned case-insensitively:
- Lat:
latitude,lat,y,ylat,geo_lat - Lon:
longitude,lon,lng,x,xlon,xlong,geo_lon,geo_lng
API Endpoints
Spatial Queries
| Endpoint | Method | Description |
|---|---|---|
/query?lat=36.5&lon=-82.5&radius=10km |
GET | Radius search (units: km, m, mi, ft) |
/query?bbox=minlat,minlon,maxlat,maxlon |
GET | Bounding box query |
/query?lat=36.5&lon=-82.5&nearest=5 |
GET | K-nearest neighbors |
/query/within |
POST | Point-in-polygon (body: GeoJSON Polygon) |
Common Query Parameters
| Param | Example | Description |
|---|---|---|
select |
select=species,observed_on |
Column allowlist |
where |
where=quality_grade:research |
Equality filter (repeatable) |
group_by |
group_by=species |
Group column |
agg |
agg=count or agg=sum:count |
Aggregation |
limit |
limit=500 |
Max rows (default 1000, cap 100000) |
format |
format=json|csv|geojson |
Output format |
Geometry Endpoints
| Endpoint | Description |
|---|---|
POST /geometry/area |
Geodesic area + perimeter of polygon(s) |
POST /geometry/convex-hull |
Convex hull from points or bbox query |
POST /geometry/centroid |
Centroid of any geometry |
POST /geometry/buffer |
Geodesic buffer around a point/polygon |
POST /geometry/dissolve |
Group-by dissolve → hull per group |
POST /geometry/simplify |
Douglas-Peucker / Visvalingam simplification |
POST /geometry/distance |
Geodesic distance + bearing between two points |
POST /geometry/bounds |
Bounding box, envelope, dimensions |
Metadata
| Endpoint | Description |
|---|---|
GET /health |
Status + uptime |
GET /schema |
Column names, types, row count |
GET /stats |
Row count, bbox, centroid, index build time |
Tech Stack
- Rust with Axum 0.8 for HTTP
- DuckDB (bundled) for file ingestion, SQL queries, and spatial R-tree indexing
- DuckDB spatial extension for R-tree index,
ST_Intersects,ST_Distance_Sphere,ST_Contains - geo crate for geodesic geometry (area, buffer, distance endpoints)
- CORS enabled, request tracing via tower-http
Examples
# Start server
# Large files — use --disk to keep DuckDB on disk and reduce RAM usage
# Radius search
# Bounding box as CSV
# Filtered + projected
# Geodesic area of a polygon
# Distance between two points
Docker
# Drop a CSV into ./data/ and start the server
# → http://localhost:8080
The docker-compose.yml mounts ./data into the container and serves whatever file you point it at. Edit the command in the compose file to change the filename or add --lat/--lon overrides.
Benchmarks
Generate test datasets — simulated iNaturalist-style wildlife observations across the Southern Appalachians:
# Standard sizes (10K / 100K / 1M)
# iNaturalist scale (250M rows, ~15 GB)
Run the benchmark suite:
Or via make: make gen to build the datasets and make bench DATASET=1m to run the suite.
Results on 1M rows (release build, single core):
| Query | Rows | Time |
|---|---|---|
| Index build (1M points) | — | 199ms |
| Nearest 10 | 10 | 11ms |
| Radius 5km | 1,000 | 31ms |
| Radius 10km | 1,000 | 79ms |
| BBox 0.2° | 1,000 | 76ms |
| Within (small polygon) | 20,522 | 119ms |
| Geometry (area/distance/buffer) | 1 | ~5ms |
| Schema / Stats / Health | — | ~5ms |
Citation
If you use Terrana in academic research, please cite it as:
Or in prose: Terrana (2026). Zero-config spatial API server. https://doi.org/10.5281/zenodo.20515989
Testing
The integration tests in tests/api.rs spawn the real binary and hit
the HTTP endpoints. They are #[ignore]d by default because starting the server
downloads the DuckDB spatial extension on first use.
Contributing
Contributions are welcome — bug reports, new file formats, geometry operations, performance work, and docs. See CONTRIBUTING.md for development setup and the pre-PR checklist, and SECURITY.md for how to report vulnerabilities.
Please run cargo fmt --all and cargo clippy --all-targets -- -D warnings before
submitting a PR.
License
Licensed under either of MIT or Apache-2.0 at your option. Unless you explicitly state otherwise, any contribution you submit for inclusion in this work shall be dual-licensed as above, without additional terms.