Documentation Index
Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt
Use this file to discover all available pages before exploring further.
For every vessel currently in transit, Axiom Overwatch projects a great-circle waypoint sequence to the vessel’s declared destination port and an ETA distribution (p10 / p50 / p90) at each waypoint. This gives downstream consumers a forward-looking view of where every active vessel is going and when it is expected to arrive — at the ship level, not just at the port aggregate level.
When to use this
- Power a vessel detail view that shows the projected path and arrival window
- Estimate inbound cargo arrivals by joining forecasts to draft-derived tonnage
- Monitor vessels that are running materially behind their declared ETA
- Pre-position alerts based on which vessels will reach a region in the next 24 / 48 / 72 hours
If you only need port-level cargo volume forecasts, use the Forecasts API. Use the route-forecast dataset when you need a per-vessel, per-waypoint breakdown.
What gets forecast
Route forecasts are produced for vessels that meet all of these conditions on the hourly run:
- Latest AIS position is less than 6 hours old
- Vessel is outside every defined
port_zone (entering port-call mechanics is out of scope)
- Vessel has a non-null AIS-declared destination string
- Resolved destination is at least 30 nautical miles away
Vessels that fail any of these checks are silently skipped on the run and re-evaluated the next hour.
The dataset
Each qualifying vessel produces one forecast row per hourly run plus eight waypoint rows keyed by forecast_id. Old rows are retained so accuracy can be backtested later.
vessel_route_forecasts
| Field | Description |
|---|
id | UUID, primary key. |
imo_number | Vessel IMO. References vessels. |
visit_id | Linked vessel_visits row when the forecast can be tied to an open visit. May be null. |
origin_lat, origin_lng | Vessel position at forecast time. |
origin_timestamp | AIS timestamp of the position the forecast was projected from. |
destination_port_id | Resolved port UUID. References ports. |
destination_raw | Raw AIS destination string (preserved for audit). |
destination_lat, destination_lng | Centroid of the resolved destination port. |
current_speed_knots | SOG at forecast time. |
total_distance_nm | Great-circle distance from origin to destination. |
forecast_method | Versioned method tag. Currently great_circle_v1. |
computed_at | Timestamp the forecast was written. |
vessel_route_forecast_waypoints
Eight rows per forecast (waypoint_index 0 through 7). Index 0 is the origin, index 7 is the destination, and the six intermediate points are spaced evenly along the great-circle arc.
| Field | Description |
|---|
forecast_id | FK to vessel_route_forecasts (cascade delete). |
waypoint_index | 0 to 7. |
lat, lng | Waypoint coordinates on the great circle. |
cumulative_distance_nm | Distance from origin along the arc. |
eta_p10 | Optimistic arrival timestamp (vessel sustains 20% above projection speed). |
eta_p50 | Median arrival timestamp at projection speed. |
eta_p90 | Pessimistic arrival timestamp (vessel runs 30% below projection speed). |
How it’s computed
Each hour at minute :25 (offset from AIS polling at :15 and the latest-positions matview refresh) the compute-route-forecasts job runs:
- Pull active vessels. Read
mv_latest_positions filtered to non-null destinations within the last 6 hours, capped at 500 vessels per run.
- Resolve the destination. Match the AIS destination string against the
ports_with_centroid view via a four-step cascade — exact slug, exact name, last whitespace-separated tail token (handles strings like NL RTM > BR SSZ), then a final ILIKE fragment match. Skip the vessel if no port resolves.
- Filter port-call mechanics. Skip vessels currently inside any
port_zone (resolved via the point_inside_any_port_zone PostGIS RPC) and vessels within 30 NM of their destination.
- Build the great-circle arc. Slerp eight waypoints between origin and destination centroid on a unit sphere.
- Compute the ETA distribution. Project ETA at each waypoint using
cumulative_distance / projection_speed. The p10 band is 20% faster than p50 and the p90 band is 30% slower (vessels lose more speed than they add).
- Persist one forecast row plus eight waypoint rows.
Projection speed
The projection speed is the vessel’s current SOG when SOG is at least 5 knots. Below that threshold the SOG is considered too noisy or stationary to extrapolate (drifting, engine-down, or spoofed-stationary) and the run falls back to a class-typical cruise speed:
| Vessel class | Fallback speed (knots) |
|---|
| Container | 18 |
| Ro-Ro | 17 |
| LNG | 16 |
| Reefer | 14 |
| Tanker | 13 |
| Bulker | 12 |
| General cargo | 11 |
| Unknown | 12 |
The same projection speed is used for every waypoint in a single forecast. The next hourly run picks up any speed change.
great_circle_v1 does not account for weather routing, traffic separation schemes, or piracy avoidance corridors. The forecast is a clean great-circle projection. A future weather_routed_v1 method will retain the same schema with a different forecast_method tag.
Querying
The dataset lives in vessel_route_forecasts and vessel_route_forecast_waypoints. Both tables are RLS-protected with read-all and service-role-write policies. Indexes on (imo_number, computed_at DESC) and (destination_port_id, computed_at DESC) cover the most common access patterns.
-- Latest forecast for one vessel, with its eight waypoints in order.
select
f.imo_number,
f.destination_port_id,
f.total_distance_nm,
f.current_speed_knots,
w.waypoint_index,
w.lat,
w.lng,
w.cumulative_distance_nm,
w.eta_p10,
w.eta_p50,
w.eta_p90
from vessel_route_forecasts f
join vessel_route_forecast_waypoints w on w.forecast_id = f.id
where f.imo_number = $1
order by f.computed_at desc, w.waypoint_index asc
limit 8;
-- All vessels with a p50 ETA inside the next 48 hours at a destination port.
select distinct on (f.imo_number)
f.imo_number,
f.current_speed_knots,
w.eta_p50 as expected_arrival,
w.eta_p10 as earliest,
w.eta_p90 as latest
from vessel_route_forecasts f
join vessel_route_forecast_waypoints w on w.forecast_id = f.id
where f.destination_port_id = $1
and w.waypoint_index = 7
and w.eta_p50 between now() and now() + interval '48 hours'
order by f.imo_number, f.computed_at desc;
-- Plot the projected great-circle path as a line for the latest forecast.
select array_agg(
array[w.lng, w.lat] order by w.waypoint_index
) as line
from vessel_route_forecasts f
join vessel_route_forecast_waypoints w on w.forecast_id = f.id
where f.imo_number = $1
and f.computed_at = (
select max(computed_at) from vessel_route_forecasts where imo_number = $1
);
Helper view and RPC
The migration ships two reusable helpers because ports itself has no lat/lng columns:
ports_with_centroid — view exposing (id, slug, name, country, lat, lng) derived from each port’s port_zones geometry. Prefers the geofence zone and falls back to any zone. SECURITY INVOKER, granted to anon, authenticated, and service_role.
point_inside_any_port_zone(p_lat numeric, p_lng numeric) — STABLE PostGIS RPC returning true when the point sits inside any defined port zone.
Both are useful outside the forecaster. Use the view whenever you need a port centroid without joining port_zones manually.
-- Top 10 ports closest to a given lat/lng.
select id, slug, name,
earth_distance(
ll_to_earth($1, $2),
ll_to_earth(lat::float, lng::float)
) / 1852 as nm_away
from ports_with_centroid
where lat is not null and lng is not null
order by nm_away asc
limit 10;
Operational notes
- Per-vessel errors (resolution failure, insert failure) are caught individually and logged. The run bails after 10 errors to avoid log floods.
- Every run writes one row to
ingestion_logs with source = 'compute_route_forecasts'. status = 'partial' indicates per-vessel errors; status = 'failed' indicates a fatal error before any vessels were processed.
- The hourly cron job is registered as
compute-route-forecasts-hourly. Re-running the migration upserts the schedule by name and is safe.