Route forecasts
Per-vessel ETA distributions and great-circle waypoint sequences for vessels in transit — methodology behind Overwatch expected arrivals and route maps.
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_positionsfiltered 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_centroidview via a four-step cascade — exact slug, exact name, last whitespace-separated tail token (handles strings likeNL RTM > BR SSZ), then a finalILIKEfragment match. Skip the vessel if no port resolves. - Filter port-call mechanics. Skip vessels currently inside any
port_zone(resolved via thepoint_inside_any_port_zonePostGIS 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'sport_zonesgeometry. Prefers thegeofencezone and falls back to any zone.SECURITY INVOKER, granted toanon,authenticated, andservice_role.point_inside_any_port_zone(p_lat numeric, p_lng numeric)—STABLEPostGIS RPC returningtruewhen 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_logswithsource = '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.
Voyage time allocation
Per-visit breakdown of arrival to departure into approach, anchor wait, berth, and idle hours — methodology behind Overwatch's voyage time-allocation analytics.
Chokepoint transits
Analyze vessel movement through four maritime chokepoints with AIS evidence, and understand the non-confirming Sentinel-1 catalog boundary.