Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
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
Terrana is three things in one: a CLI / HTTP server (above), a Rust library
(cargo add terrana), and a Python package (pip install terrana) — the same
engine, embeddable in-process or served over HTTP.
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:
As a library dependency
Terrana is a lib + bin crate — add it to your own Rust project to use its
ingestion, spatial queries, and geodesic geometry directly (see
Rust library below for usage):
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
Rust library
The same engine is available as a library — load a file, then run spatial queries and geodesic geometry without the HTTP server:
use ;
use geodesic_distance;
use Point; // Terrana's geometry operates on geo-types
// Load a CSV / Parquet / GeoJSON file and build the spatial R-tree index.
let conn = create_connection?;
let info = ingest_file?;
println!;
// Geodesic distance + bearing (WGS 84, never planar).
let d = geodesic_distance;
println!;
Spatial queries run through the DuckDB-backed builders in terrana::db::query
(bbox_filter / radius_filter + query); geodesic geometry lives in
terrana::geometry (area, buffer, hull, simplify, measure). The server
feature (the Axum router, on by default) can be disabled for a pure-library
dependency — cargo add terrana --no-default-features.
Python
Terrana ships Python bindings backed by the same Rust engine — no separate Rust toolchain needed:
The wheel is a single stable-ABI (abi3) build that runs on CPython 3.9 – 3.14+.
Command line
The wheel installs a terrana command — the same CLI as the Rust binary, with no Rust
toolchain required:
# → REST API running at http://localhost:8080
Every option the binary accepts works here too (--port, --bind, --watch, --disk,
--table, --lat, --lon) — see Usage above. Ctrl-C stops the server.
Library mode (in-process, no server)
= # also load_parquet / load_geojson
# Spatial queries take (lat, lon) — mirroring the REST API's ?lat=&lon=.
# Each row is a dict; radius/nearest add a `_distance_km` key.
= # lat, lon, radius_m
= # min_lat, min_lon, max_lat, max_lon
= # lat, lon, k
# Geodesic geometry takes (lon, lat) — GeoJSON coordinate order. GeoJSON outputs
# come back as JSON strings (use json.loads); area/distance come back as dicts.
= # lon1, lat1, lon2, lat2
=
=
= # lon, lat, distance_m
Coordinate order: spatial queries are
(lat, lon)(REST-API order); geometry functions are(lon, lat)(GeoJSON order). Mind the difference.
Server mode (embedded HTTP server)
Run the full REST API in a background thread and manage it from Python:
=
# The complete REST API is live at http://localhost:8080
=
# server is shut down and its thread joined on context exit
# Or manage the lifecycle explicitly:
=
# ... use the API ...
# stops the server and joins the thread (idempotent)
# Or block in the foreground until Ctrl-C:
# session.serve(port=8080)
The tokio runtime lives entirely inside a Rust thread, so serving never blocks Python's GIL.
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.