← Spatial Agents

Spatial Tiling

The planet is divided into a hierarchy of hexagonal cells using Uber's H3 indexing system. Each cell is a discrete, queryable unit of geography with a unique identifier.

Why Hexagons?

Hexagons tile the sphere more uniformly than squares. Every neighbor is equidistant from the center, which eliminates the directional bias inherent in rectangular grids. This matters when measuring density, proximity, and movement across cells.

H3 provides 16 resolution levels (0-15). Spatial Agents uses resolutions 3 through 7, each serving a different analytical purpose.

Resolution Pyramid

ResolutionEdge LengthUse CaseTime Bin
3~59.8 kmRegional density heatmap1 day
4~22.6 kmShipping lanes and corridors1 hour
5~8.5 kmPort and airport approach5 minutes
6~3.2 kmHarbor and terminal detail1 minute
7~1.2 kmBerth and gate levelLive

Temporal Binning

Each resolution has a paired time window. Coarse resolutions aggregate data over longer periods (daily at res 3), while fine resolutions capture near-real-time snapshots (live streaming at res 7). This pairing keeps tile sizes manageable while maximizing temporal fidelity where it matters most.

Tile File Structure

{tile_dir}/{resolution}/{cell_id}/{temporal_bin}.json

Each tile contains metadata (cell ID, resolution, counts, bounding box) plus the full vessel and aircraft records within that cell and time window.

Cell Assignment at Ingest

When a new position record arrives from any feed, it is immediately assigned H3 cell IDs at all five resolutions. This happens once at ingest time, so downstream queries by cell are simple dictionary lookups rather than spatial computations.

Multi-Region Coverage

A region is defined as one res-4 cell (the primary, where the data matters most) plus its six adjacent res-4 neighbors as a buffer ring. The seven-cell tile gives ~70 km of coverage across the region with a clean visual hierarchy: solid outline for the primary, dashed for buffers.

The active set is configured as a list — typically two slots, e.g. ["san_francisco", "chicago"] — and the AIS subscription, ADS-B poll, NWS alert filter, and FAA TFR tagging all derive their geographic filters from this single source.

Runtime Region Swap

Slot 0 is pinned to san_francisco (legacy v3.1 client contract). Slot 1 is mutable at runtime via POST /regions/swap — the server geocodes a city name through Nominatim, snaps the resulting lat/lng to the nearest res-4 H3 cell, computes the 7-cell tile, and atomically updates the active set. AIS reconnects with the new bbox union, ADS-B picks up the new region in its next rotation, and per-region caches are purged.

Why mutate at runtime instead of just adding more regions?

AIS budget. aisstream.io's free tier subscribes to a single bbox set; every additional region widens the union and increases the inbound message rate. Two regions is a comfortable steady state — ten would saturate the connection and force us into a paid tier.

ADS-B budget. OpenSky's free tier rate-limits per-IP. Each active region adds ~1 poll per 45 s rotation slot, so N regions means each region is polled every 45 × N seconds. Past three or four regions, freshness degrades faster than users tolerate.

The swap pattern is the answer. Keep the active set small (slot 0 + slot 1), and let users pivot slot 1 to whatever city they care about right now. A 112-second cooldown between swaps prevents thrashing the upstream feeds.

Visualization Pattern

Render hexes as polygons using h3_cell_to_boundary, never as marker icons centered on the cell — actual cell shapes distort with latitude.

Translucent fill + crisp outline. Fill at 8–18% opacity conveys "this area is in the set"; the outline at 70–90% opacity carries the hex shape. Solid stroke for primary cells, dashed for buffer cells.

Mixed-resolution compact sets stay mixed. When a polygon (weather alert, TFR) is converted to H3, the result is a minimal cell set at varying resolutions — render each cell at its native resolution.

API Access

EndpointReturns
/healthPer-region geometry (GeoJSON MultiPolygon), primary cell, buffer cells, H3 sample sets, advisories
POST /regions/swapReplace slot 1 with a new city. Server geocodes, computes the 7-cell tile, updates feeds. 112 s cooldown.
/regionsDiagnostic snapshot: active list, version hash, cooldown remaining
/api/tiles/info/{h3_cell}Cell metadata, center, boundary, neighbors
/api/tiles/bboxAll cells within a bounding box
/api/tiles/positionCell IDs for a lat/lng at all resolutions
/api/tiles/statsTotal tile count and storage size
/tiles/{path}Pre-computed static tile JSON files