There is no good open-source library for computing maritime routes in JavaScript. I know this because I needed one, couldn’t find one, and ended up building one from scratch.
The existing options are thin. searoute-js hasn’t been updated since 2020 and uses shipping lane data from the year 2000. It explicitly warns that it’s “not for routing purposes.” The Python variant, searoute-py, is more actively maintained but still uses the same underlying dataset and doesn’t guarantee land avoidance — routes can and do clip through coastlines.
Commercial APIs exist (Searoutes.com, Seametrix, NavAPI), but they start at EUR 600/year and require enterprise sales conversations for anything production-grade.
So we built our own ocean routing engine for ArcNautical, our maritime risk intelligence platform. After running it in production against thousands of real voyages, we extracted it into a standalone open-source library: @arcnautical/maritime-routing.
This post explains how maritime routing actually works, why it’s harder than road routing, and how to use the library in your own project.
Why ocean routing is harder than road routing
Road routing is a solved problem. OpenStreetMap gives you a complete graph of every road on Earth. OSRM or GraphHopper can route between any two addresses in milliseconds. The road network is discrete and well-defined — you’re either on a road or you’re not.
Ocean routing has none of these properties.
There are no roads. A vessel can sail in any direction, at any point in the ocean. The search space isn’t a graph with thousands of edges — it’s a continuous two-dimensional surface covering 361 million square kilometres.
Coastlines are complex. The Earth has approximately 1.6 million kilometres of coastline. A route from Singapore to Rotterdam must thread through the Malacca Strait (2.8km wide at its narrowest), transit the Suez Canal (205m wide), and pass through the Strait of Gibraltar — all without clipping through land.
Narrow passages are critical. Maritime commerce flows through a handful of chokepoints: Suez, Panama, Malacca, Hormuz, Bab el-Mandeb, Bosphorus. A routing engine that can’t navigate these isn’t useful.
The antimeridian exists. Routes crossing the 180th meridian (common for trans-Pacific voyages) need special handling to avoid rendering artifacts and incorrect distance calculations.
The approach: A* on a bitmap ocean grid
The core insight is to discretise the ocean into a bitmap — a grid of cells where each cell is either water (1) or land (0). Then you can run standard pathfinding algorithms on this grid.
Our ocean grid has these parameters:
- Resolution: 0.05° (~5.5km at the equator)
- Dimensions: 7,200 x 3,600 cells
- Source: OpenStreetMap water polygon shapefiles
- Size: 106KB compressed (gzip)
The grid is generated from OSM’s water-polygons-split-4326 dataset using a custom rasteriser. Each cell centre is tested against the water polygons using ray-casting point-in-polygon. After rasterisation, a flood fill from a known ocean point (mid-Pacific) purges inland water bodies — the Great Lakes, Caspian Sea, Lake Baikal, and others that would otherwise be navigable.
Narrow passages (Suez Canal, Panama Canal, Bosphorus, Dardanelles, Kiel Canal, Messina, Corinth) are force-carved into the grid with a 5-cell brush width, ensuring A* has enough corridor margin for path smoothing.
The pathfinding pipeline
Given two coordinates, the algorithm runs through six stages:
1. Snap to water. The start and end coordinates may fall on land (ports are typically on the coast). BFS finds the nearest water cell within ~1.5° of each point.
2. A* search. 8-directional A* with a haversine heuristic. The heuristic is admissible (haversine never overestimates) so A* is guaranteed to find the shortest path. Edge weights account for latitude — cells near the equator are wider in longitude than cells near the poles.
3. RDP smoothing. The raw A* path follows grid cells and looks jagged. Ramer-Douglas-Peucker simplification removes redundant points while preserving the route’s shape. Crucially, each simplification is corridor-validated — a shortcut is only accepted if a ±2 cell corridor along the new segment is entirely water. This prevents the smoothed path from grazing coastlines.
4. Validation and correction. The smoothed path is walked in coordinate space, sampling points along each segment. If more than 3 consecutive samples hit land, a correction point is inserted at the nearest water cell. This catches any edge cases the grid-space validation missed.
5. Densification. Points are interpolated so no two consecutive coordinates are more than 0.1° apart. This prevents Web Mercator projection distortion from bending long segments into coastlines during map rendering.
6. Longitude normalisation. Consecutive coordinates are adjusted so they never span more than 180° of longitude. This prevents MapLibre/Leaflet from rendering lines “the wrong way around” at the antimeridian.
For long routes (>5,000nm), direct A* can be slow — the grid has 25 million cells and the search space grows quadratically. The solution is recursive subdivision: split the route at the great-circle midpoint, route each half independently, and concatenate. This reduces individual A* searches to manageable distances while maintaining correctness.
Performance: 8–50ms for typical segments. 1–4 seconds for very long trans-oceanic routes (via recursive subdivision).
Waypoint routing with Dijkstra
A* on the grid gives you a land-free path between two points, but it doesn’t know about maritime conventions. Commercial vessels don’t just draw a straight line across the ocean — they follow established shipping lanes that pass through specific chokepoints.
To handle this, the library includes a strategic waypoint graph: 48 nodes representing major straits, canals, and capes, connected by 78 distance-weighted edges. Dijkstra’s algorithm finds the shortest path through this graph, then each waypoint-to-waypoint segment is routed through the A* ocean pathfinder.
The waypoint graph handles region classification (16 ocean regions), so a port in the Mediterranean connects to different waypoints than a port in the Persian Gulf. Same-region ports can route directly without any waypoint transits.
Avoid zones
The routing engine supports polygon avoid zones. Any waypoint-graph edge that intersects an avoid-zone polygon receives a 100x distance penalty, effectively routing around it. This is how you route around piracy areas, conflict zones, or sanctioned EEZs.
The port database
The library includes 510+ major commercial ports worldwide, each with:
- UN/LOCODE (e.g.,
SGSINfor Singapore) - Coordinates (lat/lon)
- Country code (ISO 3166-1)
- Port type (container, bulk, tanker, LNG, naval, mixed, general)
- Ocean region classification
Port search supports fuzzy matching by name, country, or LOCODE. There’s also an AIS destination resolver that can parse the messy free-text destination fields in AIS transponder data (">>SGSIN<<", "NL RTM", "ROTT" all resolve correctly).
Weather-aware ETA
Distance alone doesn’t give you an accurate ETA. A bulk carrier doing 14 knots in calm seas might only manage 10 knots in Beaufort 7 conditions.
The speed model implements Kwon 2008 involuntary speed loss factors, indexed by Beaufort number for five vessel types (container, bulk, tanker, LNG, general) and two load conditions (laden, ballast). When wave direction data is available, the Kwon Cbeta correction factors reduce speed loss for beam and following seas compared to head seas.
import { computeSeaStateFactor } from '@arcnautical/maritime-routing';
const result = computeSeaStateFactor({
baseSpeedKnots: 14,
waveHeightM: 3.0,
windSpeedKt: 25,
swellHeightM: 1.5,
vesselType: 'bulk',
loadCondition: 'laden',
});
console.log(result.effectiveSpeedKnots); // 11.76
console.log(result.beaufortNumber); // 6
console.log(result.speedReductionPct); // 16%
Using the library
Install:
npm install @arcnautical/maritime-routing
Compute a route between two ports
import { computeRoute } from '@arcnautical/maritime-routing';
const route = computeRoute('SGSIN', 'NLRTM');
console.log(route.distance_nm); // 7991.1
console.log(route.duration_hours); // 570.8 (at 14kt)
console.log(route.hazard_zones_crossed); // ['Malacca Strait', 'Suez Canal', ...]
console.log(route.route_geojson); // GeoJSON FeatureCollection
Raw ocean pathfinding
import { findOceanPath } from '@arcnautical/maritime-routing';
// Returns [lon, lat][] — GeoJSON convention
const path = findOceanPath(1.26, 103.84, 51.90, 4.50);
// ~500 coordinate pairs, guaranteed to never cross land
Search ports
import { searchPorts, getPortByLocode } from '@arcnautical/maritime-routing';
const results = searchPorts('Singapore');
// [{ locode: 'SGSIN', name: 'Singapore', country: 'Singapore', ... }]
const port = getPortByLocode('NLRTM');
// { name: 'Rotterdam', lat: 51.90, lon: 4.50, region: 'north_sea', ... }
EEZ transit analysis
import { computeRouteInternal, analyzeRouteEezTransit } from '@arcnautical/maritime-routing';
const route = computeRouteInternal('SGSIN', 'AEJEA');
const eez = analyzeRouteEezTransit(route.segments, route.totalDistanceNm);
for (const zone of eez.transitEezs) {
console.log(`${zone.countryName}: ${(zone.routeFraction * 100).toFixed(1)}%`);
if (zone.isSanctioned) console.log(' SANCTIONED ZONE');
}
Try it live
We built an interactive demo where you can compute routes between any two ports and see them rendered on a map in real-time:
The demo also shows the voyage risk score for each route — that’s powered by ArcNautical’s commercial risk intelligence engine, which layers piracy data, sanctions screening, conflict indices, and weather forecasts on top of the routing.
Comparison with alternatives
| @arcnautical/maritime-routing | searoute-js | searoute-py | |
|---|---|---|---|
| Language | TypeScript | JavaScript | Python |
| Last updated | 2026 | 2020 | 2026 |
| Dependencies | Zero | Multiple | NetworkX |
| Land avoidance | Guaranteed (bitmap) | No (graph only) | No |
| Port database | 510+ ports | None | None |
| Weather model | Beaufort + Cbeta | None | None |
| EEZ analysis | Yes | None | None |
| Avoid zones | Yes | No | Passage restrictions |
| Data source | OSM (2024) | ORNL (2000) | ORNL (2000) |
| Resolution | 0.05° (~5.5km) | Variable | Variable |
Source and license
The library is MIT licensed. The ocean grid is derived from OpenStreetMap data (ODbL). The speed model tables are from published academic literature. All port data is based on the public UN/LOCODE standard.
- npm: @arcnautical/maritime-routing
- GitHub: github.com/SaltyTaro/maritime-routing
- Interactive demo: arcnautical.com/routing-demo
If you need maritime risk intelligence beyond routing — voyage risk scoring, sanctions screening, fleet monitoring, automated compliance reports — that’s what ArcNautical does.