npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

searoute-ts

v2.2.0

Published

Shortest sea route between any two points on Earth. TypeScript library with the Eurostat 2025 maritime network, canal/strait restrictions (Suez, Panama, Bab-el-Mandeb…), vessel-draft gating, K-shortest alternatives, multi-leg waypoints, and ETA from vesse

Readme

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.

npm version npm downloads CI license types

npm install searoute-ts
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)

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 alternativesseaRouteAlternatives returns the baseline plus up to N realistic alternatives.
  • 🧭 Multi-leg waypointsseaRouteMulti for port rotations and itineraries.
  • ⏱️ ETA from speedspeedKnotsdurationHours.
  • 🛠️ 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/[email protected]/…') | 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/[email protected]/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.

Output shape

{
  type: 'Feature',
  geometry: { type: 'LineString', coordinates: [[lon, lat], ...] },
  properties: {
    length: number,                    // in `units`, in-water only
    units: 'nauticalmiles' | 'kilometers' | 'miles' | ...,
    bbox: [minLon, minLat, maxLon, maxLat],
    greatCircleLength: number,         // haversine between inputs, same units
    detourRatio: number,               // routeKm / greatCircleKm
    originSnapKm: number,              // input → network distance
    destinationSnapKm: number,
    durationHours?: number,            // if `speedKnots` set
    passages?: ('suez' | 'panama' | ...)[],  // if `returnPassages: true`
  }
}

Full options

seaRoute(origin, destination, {
  units:                   'nauticalmiles',          // any Turf unit
  restrictions:            ['suez', 'babelmandeb'],  // see passage table below
  allowArctic:             false,                    // default — blocks NWP & NEP
  vesselDraftMeters:       15,                       // auto-restrict canals
  speedKnots:              22,                       // → properties.durationHours
  appendOriginDestination: false,                    // prepend/append raw inputs
  returnPassages:          true,                     // populate properties.passages
  maxSnapDistanceKm:       50,                       // SnapFailedError if exceeded
  network:                 customMarnet,             // BYO FeatureCollection
  antimeridian:            'split',                  // 'unwrap' | 'split' dateline handling
});

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.

Antimeridian (dateline) handling

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:

seaRoute(yokohama, la, { antimeridian: 'unwrap' }); // continuous LineString (may exceed ±180)
seaRoute(yokohama, la, { antimeridian: 'split' });  // MultiLineString cut at ±180 (RFC 7946)

'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.

Restrictable passages

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.

Validated against industry distances

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.

Errors

  • 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).

API reference

import {
  seaRoute,                  // single shortest route
  seaRouteMulti,             // ordered waypoints (multi-leg)
  seaRouteAlternatives,      // K-shortest alternatives
  loadNetwork,               // optional: fetch a network from a URL/CDN
  CANAL_MAX_DRAFT_M,         // { panama: 15.2, suez: 20.1, kiel: 7, corinth: 7.3 }
  DEFAULT_MARNET,            // bundled FeatureCollection<LineString>
  PASSAGE_BBOXES,            // passage bbox lookup
  clearFinderCache,          // drop the PathFinder cache (tests / hot reload)
  SnapFailedError,
  NoRouteError,
  UnknownPortError,          // thrown for unresolved UN/LOCODE strings
  registerPortResolver,      // plug in a custom port dataset
  // types
  type Passage,
  type Antimeridian,
  type SeaRouteOptions,
  type SeaRouteFeature,
  type SeaRouteMultiFeature,
  type SeaRouteProperties,
  type LoadNetworkOptions,
  type MarnetNetwork,
  type MarnetProperties,
} from 'searoute-ts';

import {
  lookupPort,                // UN/LOCODE → { code, name, country, coordinates }
  resolvePort,               // UN/LOCODE → [lon, lat]
  PORTS,                     // the raw dataset (Record<code, PortRecord>)
  PORT_COUNT,
  type Port,
  type PortRecord,
} from 'searoute-ts/ports';

Port codes (UN/LOCODE)

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.

  • ~1 600 seaports, keyed by UN/LOCODE (primary codes and aliases).
  • Source: marchah/sea-ports (MIT), itself derived from UN/LOCODE. Regenerate with scripts/build-ports.cjs.
  • Coordinates are approximate (port-city granularity) — the routing engine snaps them onto the network anyway, so this is fine for distance/visualisation.
  • Unknown or malformed codes throw UnknownPortError.

How it works

A two-page deep-dive (graph data, snapping, Dijkstra, restrictions, antimeridian fix, draft logic, alternatives) is in DOCS.md.

FAQ

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 higher-resolution Eurostat data (5/10/20/50 km), inland waterways, or AIS-derived custom graphs.

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? ~184 KB packed / ~1.3 MB unpacked on npm. The bundled marnet is the bulk (~1.1 MB JSON). It ships once as a shared dist/data/marnet.cjs asset that both the CJS and ESM builds load at runtime, rather than being inlined into each build. Tree-shakeable.

Credits

License

MIT © Mayur Rawte