searoute-ts
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.

import { seaRoute } from 'searoute-ts';
const route = seaRoute([121.5, 31.0], [4.4, 51.9]);
// Shanghai β Rotterdam β GeoJSON LineString, ~10,664 nm via Suez Canal
πΊοΈ Try the interactive demo β click two points on a map and see the route, with all options live. (source)
π Port-to-port sea distances β distance, sailing time and canal passages for common routes between major ports.
Works from plain JavaScript too β the package ships compiled .js plus
.d.ts declarations. The -ts in the name is for searchability, not a
language requirement.
Why searoute-ts
- π’ Realistic shipping routes, not great-circle lines through Eurasia.
- πΊοΈ Returns GeoJSON β drop straight into Leaflet, Mapbox, deck.gl, MapLibre.
- π 2025 Eurostat marnet with explicit Suez, Panama, Bab-el-Mandeb,
Malacca, Gibraltar, Dover, Kiel, Corinth, Bering, Magellan, NW/NE Passage labels.
- π« Canal & strait restrictions β force Cape of Good Hope during a Red Sea
disruption with one option.
- π¦ Vessel-draft gating β auto-block Panama (15.2 m), Suez (20.1 m), Kiel
(7 m), Corinth (7.3 m) when the vessel exceeds the canal limit.
- π€οΈ K-shortest alternatives β
seaRouteAlternatives returns the baseline
plus up to N realistic alternatives.
- π§ Multi-leg waypoints β
seaRouteMulti for port rotations and itineraries.
- β±οΈ ETA from speed β
speedKnots β durationHours.
- π οΈ Modern toolchain β TypeScript 5, ESM + CJS dual build, types included,
Node 18+, zero peer deps.
Quick examples
Basic β shortest route
import { seaRoute } from 'searoute-ts';
const route = seaRoute([-74.04, 40.69], [-0.13, 51.5]); // NYC β London
// route.properties.length // β 3 362 nm
// route.properties.units // 'nauticalmiles'
With ETA and units
seaRoute(shanghai, rotterdam, {
units: 'kilometers',
speedKnots: 22,
});
// β 19 753 km, properties.durationHours β 485 h (β 20 days)
Red Sea / Suez disruption β force Cape of Good Hope
seaRoute(shanghai, rotterdam, {
restrictions: ['suez', 'babelmandeb'],
});
// β routes via Cape of Good Hope, ~25 800 km
Vessel-aware β Ultra Large Container Ship
seaRoute(shanghai, newYork, {
vesselDraftMeters: 16, // exceeds Panama's 15.2 m TFW
});
// β Panama auto-blocked, route goes via Suez
Port codes (UN/LOCODE)
import 'searoute-ts/ports'; // enables UN/LOCODE strings on the core API
import { seaRoute } from 'searoute-ts';
seaRoute('CNSHA', 'NLRTM'); // Shanghai β Rotterdam
seaRoute('CNSHA', [4.4, 51.9]); // mixing a code and coordinates is fine too
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:
import { lookupPort, resolvePort } from 'searoute-ts/ports';
lookupPort('SGSIN'); // β { code, name: 'Singapore', country, coordinates: [lon, lat] }
resolvePort('SGSIN'); // β [103.85, 1.28]
Unknown codes throw UnknownPortError. See Port codes below for provenance.
Load the port dataset from a CDN instead of bundling it
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:
import { seaRoute, loadPorts } from 'searoute-ts';
// Pin a version for reproducibility, or use @latest to always get the newest.
await loadPorts('https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/ports.json');
seaRoute('CNSHA', 'NLRTM'); // works β the fetched dataset is now registered
https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/ports.json # newest
https://cdn.jsdelivr.net/npm/searoute-ts@<version>/dist/ports.json # frozen/immutable
(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.
Multi-leg / port rotation
import { seaRouteMulti } from 'searoute-ts';
seaRouteMulti(
[shanghai, singapore, mumbai, rotterdam],
{ units: 'kilometers', returnPassages: true },
);
// β one concatenated LineString, total length, union of passages
Alternative routes (Yen-style canal permutation)
import { seaRouteAlternatives } from 'searoute-ts';
const alts = seaRouteAlternatives(shanghai, rotterdam, { k: 4 });
// baseline 19 753 km via Suez
// no-malacca 20 759 km
// no-suez 25 315 km (via Panama)
// no-suez-no-panama 25 845 km (via Cape of Good Hope)
Fetch the network from a URL instead of bundling it (optional)
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:
import { seaRoute, loadNetwork } from 'searoute-ts';
// CORS-enabled, served from GitHub Pages (or point at your own host / a CDN).
const network = await loadNetwork('https://mayurrawte.github.io/searoute-ts/marnet.json');
const route = seaRoute(shanghai, rotterdam, { network });
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.
Which option should I use?
| 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 |
Versioning the hosted network
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:
https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/marnet.json # newest
https://cdn.jsdelivr.net/npm/searoute-ts@2/dist/marnet.json # newest 2.x
https://cdn.jsdelivr.net/npm/searoute-ts@2.0.1/dist/marnet.json # frozen
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.
Higher-resolution networks (optional)
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:
import { DEFAULT_MARNET } from 'searoute-ts/marnet-20km'; // or 'searoute-ts/marnet-50km'
import { seaRoute } from 'searoute-ts';
seaRoute(origin, destination, { network: DEFAULT_MARNET });
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 |