The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Searoute listing page.
Shortest sea route between any two points on Earth. A TypeScript / JavaScript library for maritime route planning, port-to-port distance, ETA estimation, and shipping-lane visualisation — powered by the 2025 Eurostat maritime network.
🗺️ Try the interactive demo — click two points on a map and see the route, with all options live. (source)
Works from plain JavaScript too — the package ships compiled
.jsplus.d.tsdeclarations. The-tsin the name is for searchability, not a language requirement.
seaRouteAlternatives returns the baseline
plus up to N realistic alternatives.seaRouteMulti for port rotations and itineraries.speedKnots → durationHours.The ~1 600-port dataset lives behind the searoute-ts/ports subpath so the core
stays lean — importing it registers the resolver. You can also resolve codes
yourself:
Unknown codes throw UnknownPortError. See Port codes below for provenance.
Don't want to bundle the ~135 KB dataset? Fetch it at runtime with loadPorts —
the analog of loadNetwork.
The dataset also ships as a raw dist/ports.json, so jsDelivr/unpkg serve it
versioned for free:
(dist/ports.json ships from the release that adds port codes onward — pin any
version at or after it for reproducibility.)
loadPorts registers the fetched dataset (so code strings resolve) and returns
it. It uses the global fetch (Node ≥18 / browsers); pass { fetch } to override.
The network is bundled by default, so seaRoute works offline with zero setup.
If you'd rather not ship the ~1 MB network (e.g. to trim a browser bundle,
or to use an updated network without upgrading the package), fetch it at
runtime and pass it via the existing network option:
Only the fetch is async — seaRoute itself stays synchronous. loadNetwork
uses the global fetch (Node ≥18 and all browsers); pass { fetch } to supply
your own. This is purely opt-in; nothing changes if you don't use it.
| Approach | How | Data version | Works offline | Best for |
|---|---|---|---|---|
| Bundled (default) | seaRoute(a, b) — no network | pinned to your installed package | ✅ | Most users; zero config, deterministic |
| Latest via URL | loadNetwork('…/marnet.json') | always the newest hosted | ❌ needs network | Always-current data without upgrading |
| Pinned via CDN | loadNetwork('https://cdn.jsdelivr.net/npm/searoute-ts@2.0.1/…') | frozen (immutable) | ❌ needs network | Reproducible builds |
You choose the version by choosing the URL:
@latest / rolling — the GitHub Pages URL above always serves the current
network. Convenient, but it can change under you.
Pinned & immutable — because the package is on npm, jsDelivr and unpkg serve every published version automatically, with immutable per-version URLs:
A pinned URL never changes, so your routes stay reproducible. (These standalone-JSON CDN paths land with the package once the network ships as a separate asset — see issue #10; until then, use the GitHub Pages URL.)
For production, prefer a pinned URL (or just the bundled default) so your distances don't shift when the network is updated.
The bundled network is Eurostat's 100 km marnet_plus. Eurostat also
publishes finer resolutions, which give more accurate coastal routing and
shorter-hop fidelity at the cost of a larger download and slightly slower
first-route graph construction. Two moderate resolutions ship as subpath
exports so you only pay for them if you import them:
Like the bundled default, each variant ships once as a shared
dist/data/marnet-<res>.cjs asset that both the CJS and ESM builds load at
runtime, so importing a variant doesn't duplicate the network across builds.
| Import | Resolution | Segments | JSON size | gzipped | Coastal accuracy |
|---|---|---|---|---|---|
searoute-ts (bundled default) | 100 km | 9,847 | ~1.3 MB | ~0.18 MB | Baseline — good for global routing |
searoute-ts/marnet-50km | 50 km | 15,498 | ~1.9 MB | ~0.27 MB | Modest step up |
searoute-ts/marnet-20km | 20 km | 29,581 | ~3.6 MB | ~0.51 MB | Noticeably finer coastal hops |
via loadNetwork (see below) | 10 km | 48,301 | ~5.9 MB | ~0.84 MB | High — larger download |
via loadNetwork (see below) | 5 km | 72,478 | ~9.0 MB | ~1.24 MB | Highest — largest download |
The 10 km and 5 km networks are large enough that bundling them would dominate
the install, so they are not shipped in the package. Generate them from the
Eurostat source with scripts/build-marnet.cjs (the script header documents the
GDAL conversion), host the resulting JSON, and load it with
loadNetwork
— or pass any FeatureCollection<LineString> to the network option directly.
Inputs can be [lon, lat] arrays, GeoJSON Feature<Point>, bare Point objects,
or a UN/LOCODE string (e.g. 'CNSHA') once searoute-ts/ports is imported.
Routes that cross the ±180° meridian (e.g. Yokohama → LA) come back wrapped to
[-180, 180] by default, which many map renderers draw as a straight streak
across the whole map. Pass antimeridian to get map-ready geometry:
'unwrap' shifts longitudes by multiples of 360° so the line never jumps the
dateline (ideal for MapLibre/Leaflet/Deck.gl). 'split' cuts the route into a
MultiLineString at ±180°, keeping every coordinate in range. Both apply to
seaRoute and seaRouteMulti; properties.length is unchanged either way.
via)restrictions blocks a passage; via requires one — the inverse. Use
it to compare explicit routings, e.g. "via Suez" against "via Cape of Good Hope",
or to force a Pacific + Panama routing between Asia and Europe:
via accepts the same passage names as restrictions and visits multiple
passages in the order given. It routes origin → passage → destination through
each passage's location using the multi-leg machinery, so it composes with the
other options. A passage named in via is never blocked out from under the
requirement (via: ['northeast'] reaches the Northeast Passage without also
needing allowArctic). Naming the same passage in both via and restrictions
is a contradiction and throws NoRouteError.
Opt in with emissions: true for two rough estimates on properties:
ecaKm — how much of the route lies inside ECA/SECA emission-control
areas (Baltic, North Sea, Mediterranean, North American and US Caribbean),
which drives fuel-type/cost. The zones ship behind the searoute-ts/eca
subpath export (to keep the core lean); importing it registers them. They are
bounding-box approximations of the IMO MARPOL Annex VI areas — good for
estimates, not compliance. Swap in higher-fidelity polygons with
registerEcaZones.co2eTonnes — a deliberately simple distance × vessel-class factor
estimate, not a certified figure. Factors are derived transparently from a
representative fuel burn and the IMO HFO CO₂ conversion (see VESSEL_CLASSES);
override with co2eFactorKgPerKm. GLEC recommends inflating shortest-path
distance by ~15 % for real-world deviations — pass glecInflation: 0.15.The first twelve are natively labelled in the Eurostat marnet (exact match
on the feature's pass attribute). The remaining four are detected via
bounding boxes.
| Name | Type | Notes |
|---|---|---|
suez | native | Suez Canal |
panama | native | Panama Canal |
gibraltar | native | Strait of Gibraltar |
babelmandeb | native | Bab-el-Mandeb (babalmandab alias) |
malacca | native | Malacca Strait |
dover | native | Dover Strait |
kiel | native | Kiel Canal |
corinth | native | Corinth Canal |
bering | native | Bering Strait |
magellan | native | Strait of Magellan |
northwest | native | Northwest Passage (blocked by default) |
northeast | native | Northeast Passage (blocked by default) |
bosporus | bbox | Bosphorus |
ormuz | bbox | Strait of Hormuz |
sunda | bbox | Sunda Strait |
cape_horn | bbox | Cape Horn region |
The Northwest and Northeast Passages are mathematically the shortest path for
many Asia ↔ Europe routes but are ice-blocked most of the year, so they are
blocked by default. Opt in with allowArctic: true.
12 real-world lanes within ±10% of published Searoutes / Sea-Distances figures.
| Lane | searoute-ts | Industry ref. |
|---|---|---|
| Shanghai → Rotterdam (Suez) | 19 753 km | ~19 300 km |
| Singapore → Rotterdam (Suez) | 15 630 km | ~15 500 km |
| Mumbai → Rotterdam (Suez) | 11 918 km | ~11 800 km |
| NY → Rotterdam | 6 227 km | ~6 200 km |
| NY → LA (Panama) | 9 154 km | ~9 100 km |
| Yokohama → LA | 9 145 km | ~8 800 km |
| Singapore → LA (trans-Pacific) | 14 364 km | ~14 300 km |
| Caldera (CL) → Bahía Blanca (AR) | 4 810 km | ~5 180 km |
All checks pass in the test suite.
SnapFailedError — input cannot be projected onto the network within
maxSnapDistanceKm. Carries .side: 'origin' | 'destination' and
.distanceKm: number.NoRouteError — no path exists between the snapped origin and destination
(e.g. all viable canals blocked).Origins and destinations may be given as UN/LOCODE strings (e.g. 'CNSHA')
instead of coordinates. The port dataset ships behind the searoute-ts/ports
subpath export, so consumers only pay for it if they use it — importing the
subpath (for any of its exports, or purely for its side effect) registers a
resolver into the core so seaRoute('CNSHA', 'NLRTM') works.
scripts/build-ports.cjs.UnknownPortError.A companion Model Context Protocol server,
@searoute-ts/mcp (source),
lets AI agents (Claude Desktop, the claude CLI, etc.) compute real sea routes
instead of guessing — asking "how far is Shanghai to Rotterdam by sea, avoiding
Suez?" calls the library directly. It exposes two tools, sea_route and
sea_route_alternatives, and accepts port codes ('CNSHA') or coordinates.
Or add it to any MCP client config:
See the server's README
for the full tool reference. For the rail leg, add
@railroute-ts/mcp alongside it
(claude mcp add railroute -- npx -y @railroute-ts/mcp).
Sea distance is rarely the whole shipment. The sibling library
railroute-ts routes over the
OpenStreetMap rail network (Europe bundled, same API shape, same GeoJSON
output), so a port-to-inland quote or a GLEC/CountEmissions-style report is one
extra call:
npm install railroute-ts — docs & interactive demo.
Both libraries also ship MCP servers, so an AI agent can chain sea_route →
rail_route for door-to-door distance (see below).
A two-page deep-dive (graph data, snapping, Dijkstra, restrictions, antimeridian fix, draft logic, alternatives) is in DOCS.md.
Is this for navigation? No. The routes are network paths suitable for visualisation and rough distance/duration estimates, not for piloting ships.
Does it support weather routing? No. For weather-aware routing see VISIR-2.
Why are my Asia→Europe routes going through Bering Strait? They aren't,
by default — the Northwest and Northeast Passages are blocked. Pass
allowArctic: true to enable them.
Can I use my own network? Yes — seaRoute(origin, destination, { network }).
Useful for inland waterways or AIS-derived custom graphs. For higher-resolution
Eurostat data (5/10/20/50 km), see
Higher-resolution networks — 20 km and
50 km ship as subpath exports.
Does it handle the Red Sea / Suez crisis? Yes — pass
restrictions: ['suez', 'babelmandeb'] to force Cape of Good Hope routing.
Is the great-circle distance correct across the antimeridian? Yes — the marnet has been normalised so the Pacific is a connected graph, and all distances use haversine internally.
What's the bundle size? What you import at runtime is small: the core plus
the bundled 100 km marnet (~1.1 MB JSON, shipped once as a shared
dist/data/marnet.cjs asset both builds load, rather than inlined into each).
Tree-shakeable, so the optional searoute-ts/marnet-20km / marnet-50km
networks only load if you import them. They do add to the npm tarball, though —
including them the package is ~1.1 MB packed / ~7 MB unpacked (each variant is a
single shared asset, not duplicated per build). If you need the finer networks
without the install cost, generate and host them and use loadNetwork instead.
geojson-path-finder@2 by Per Liedman.searoute-py (Apache-2.0).MIT © Mayur Rawte