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

gtfs-sqljs-raptor

v0.2.0

Published

Bridge a gtfs-sqljs instance to raptor-journey-planner: build raptor inputs from SQLite-backed GTFS and hydrate the resulting Journey[] with stop and route metadata. Works in browser and Node.

Readme

gtfs-sqljs-raptor

Live demo

Bridge between gtfs-sqljs (a SQLite-backed GTFS loader that runs in browsers and Node) and raptor-journey-planner (an in-memory implementation of the Raptor journey-planning algorithm).

Two pure functions:

  • buildRaptorInputs(gtfs) — turn a GtfsSqlJs instance into the [trips, transfers, interchange] triple raptor expects.
  • hydrateJourneys(gtfs, journeys) — replace raptor's bare stop/trip IDs with full Stop and Route records pulled from the same SQLite database.

Both work in browsers and Node.

Why

raptor-journey-planner's bundled loadGTFS() reads a Node Readable stream through gtfs-stream, so it cannot run in a browser. Its result is also referentially minimal — Journey.legs[] carry only StopID strings and a stripped Trip record, no stop names, no route, no headsign. This package fills both gaps.

Install

npm install gtfs-sqljs-raptor gtfs-sqljs raptor-journey-planner
# Pick one gtfs-sqljs adapter:
npm install sql.js                 # browser / Node WASM
# or: npm install better-sqlite3  # Node native

gtfs-sqljs and raptor-journey-planner are peer dependencies. raptor-journey-planner is GPL-3.0; this wrapper is MIT but your installed combination is governed by the strictest of the licences.

Usage

import { GtfsSqlJs } from 'gtfs-sqljs';
import { createSqlJsAdapter } from 'gtfs-sqljs/adapters/sql-js';
import {
  RaptorAlgorithmFactory,
  GroupStationDepartAfterQuery,
  JourneyFactory,
} from 'raptor-journey-planner';
import { buildRaptorInputs, hydrateJourneys } from 'gtfs-sqljs-raptor';

const adapter = await createSqlJsAdapter();
const gtfs = await GtfsSqlJs.fromZip('https://example.com/gtfs.zip', { adapter });

const { trips, transfers, interchange } = await buildRaptorInputs(gtfs, {
  bridgeSameNameStops: true,  // see notes below
});
const raptor = RaptorAlgorithmFactory.create(trips, transfers, interchange);
const query = new GroupStationDepartAfterQuery(raptor, new JourneyFactory());

const rawJourneys = query.plan(
  ['174', '321'],                    // origin platform stop_ids
  ['278', '416'],                    // destination platform stop_ids
  new Date('2026-05-27T12:00:00Z'),  // see "Dates" below
  9 * 3600,                          // depart-after time, seconds since midnight
);

const journeys = await hydrateJourneys(gtfs, rawJourneys);
for (const j of journeys) {
  for (const leg of j.legs) {
    if (leg.type === 'timetable') {
      console.log(`${leg.trip.route.route_short_name}: ${leg.origin.stop_name} → ${leg.destination.stop_name}`);
    } else {
      console.log(`walk ${leg.duration}s: ${leg.origin.stop_name} → ${leg.destination.stop_name}`);
    }
  }
}

API

buildRaptorInputs(gtfs, options?)

Returns Promise<{ trips, transfers, interchange }>:

  • trips: Trip[] — raptor's Trip shape, stop_times ordered by stop_sequence, times converted to seconds since midnight via raptor's TimeParser. Each trip has its Service reconstructed from calendar + calendar_dates.
  • transfers: TransfersByOrigin — raptor's Record<from_stop_id, Transfer[]>. Sourced from transfers.txt (where present) plus any synthetic bridges enabled via options.
  • interchange: InterchangeRecord<stop_id, seconds> wrapped in a Proxy so that any stop not explicitly listed resolves to defaultInterchangeSeconds (default 0). Without this, raptor's RouteScanner reads undefined for stops without an explicit interchange and arrival times become NaN.

Options:

| Option | Default | Effect | | --- | --- | --- | | bridgeSameNameStops | false | Adds Transfer rows between every pair of stops sharing the same stop_name and within sameNameMaxMeters. Many feeds split a logical station into per-route platforms without using parent_station; without bridging, raptor cannot change routes there. | | sameNameMaxMeters | 250 | Distance ceiling for bridgeSameNameStops. | | walkingSpeedMps | 1.2 | Walking speed used to convert geo distance to seconds. | | transferFallbackSpeedMps | 0.8 | Walking speed used to price transfers.txt rows whose min_transfer_time is empty/NULL. The GTFS spec leaves the time unspecified for those rows; raptor needs a number. Pricing them by haversine distance ÷ this speed approximates real-world non-straight walking and prevents the planner from chaining many "free" transfer edges into long zero-second walks. Set to null to fall back to 0 seconds (legacy behaviour). | | bridgeParentStations | false | Adds zero-duration transfers between parent_station ↔ children. Largely cosmetic — see the gotcha below. | | defaultInterchangeSeconds | 0 | Interchange time for stops not in transfers.txt. |

planByCoordinates(params)

Plan an itinerary between two arbitrary geographic coordinates that are not in stops.txt (a typed address, a pin on a map, anything with a lat/lon). Returns a Journey[] from raptor-journey-planner.

The function takes a RaptorInputs (built once), an origin and destination coordinate, and the lists of nearby real stops the planner is allowed to walk to/from at each end. The planner picks the best nearby stop on each side itself, taking walking time into account — so for a given origin coordinate, the cheapest combined (walk + transit + walk) itinerary wins, not just the geographically closest stop.

How it works internally: per query, two phantom trips are appended to a clone of inputs.trips (one per endpoint coordinate, with pickUp: false / dropOff: false so the algorithm never tries to board them) plus a few walking edges into a clone of inputs.transfers. RaptorAlgorithmFactory.create then sees the coordinates as first-class stops, but no real journey can ever traverse the phantom trips. The base inputs are not mutated — the function is safe to call concurrently for different queries.

import {
  buildRaptorInputs,
  findNearbyStops,
  loadStopLocations,
  planByCoordinates,
  hydrateJourneys,
} from 'gtfs-sqljs-raptor';

const inputs = await buildRaptorInputs(gtfs, { bridgeSameNameStops: true });
const stops = await loadStopLocations(gtfs); // one SQL pass, cache it

const origin      = { id: '__origin__',      lat: -21.28663, lon: 55.40921 };
const destination = { id: '__destination__', lat: -20.87877, lon: 55.44845 };

const findOpts = { radiusMeters: 1500, walkingSpeedMps: 1.2, maxNearbyStops: 12 };
const journeys = planByCoordinates({
  inputs,
  origin,
  destination,
  originNearby:      findNearbyStops(origin,      stops, findOpts),
  destinationNearby: findNearbyStops(destination, stops, findOpts),
  date: new Date('2026-05-04T12:00:00Z'),
  departAfterSeconds: 8 * 3600,
});

// First and last legs reference the input ids (synthetic walks).
// hydrateJourneys can't look those up — strip them off and render the
// outer walks from the input coordinates yourself.
const middle = journeys[0].legs.slice(1, -1);
const hydrated = await hydrateJourneys(gtfs, [{ ...journeys[0], legs: middle }]);

The id on Coordinate is an internal handle for the synthetic phantom stop; it must be different between origin and destination, and must not collide with any real stop_id in your feed. Pick something obviously synthetic (e.g. '__origin__') — it shows up in the returned journey's outer walking legs so callers can recognise them when stripping for hydration.

scripts/coordinate-demo.mjs is a runnable example over the Car Jaune fixture.

Helpers

findNearbyStops(point, stops, options?) — linear-scan haversine lookup. Sorts closest first, caps at maxNearbyStops (default 8). Default radius 400 m, default walking speed 1.2 m/s. For larger feeds, plug in a kd-tree or geohash index and build the NearbyStop[] array yourself.

loadStopLocations(gtfs) — convenience: one SQL pass over stops.stop_lat / stop_lon, returns { id, lat, lon }[]. Run once at startup, hand the result to findNearbyStops per query.

Performance

Per query: clone {trips, transfers, interchange}, append 2 phantom trips and a handful of walking edges, call RaptorAlgorithmFactory.create, run the query. On the Car Jaune feed (~2k trips, ~33k stop_times) the whole thing runs in ~70 ms — independent of how many candidate origin/destination coordinates you might have on file, because only the two for the current query are ever passed in. On Astuce (Rouen, ~22k trips, ~600k stop_times) the per-query cost is ~115 ms.

hydrateJourneys(gtfs, journeys)

Returns Promise<HydratedJourney[]>. Each leg becomes either:

{ type: 'timetable',
  origin: Stop, destination: Stop,
  stopTimes: { stop: Stop, arrivalTime, departureTime, pickUp, dropOff }[],
  trip: { tripId, serviceId, headsign, directionId, shortName, route: Route },
  departureTime, arrivalTime }

or

{ type: 'transfer',
  origin: Stop, destination: Stop,
  duration, startTime, endTime }

Stop and Route are the gtfs-sqljs types (snake_case fields like stop_id, route_short_name).

Lookups are batched: one query for all referenced stops, one for all referenced trips JOINed with routes. Hydration cost scales with the size of the result set, not the size of the feed.

Gotchas

Origins and destinations must appear in stop_times

raptor-journey-planner's ScanResultsFactory initializes its tracking only for stops it sees while walking trip stop_times. Pure parent stations (location_type=1, never referenced in stop_times) cannot be passed as origin or destination — raptor will throw Cannot convert undefined or null to object. Filter them out before calling query.plan():

const origins = (await gtfs.getStops({ name: 'My Station' }))
  .filter((s) => s.location_type !== 1)
  .map((s) => s.stop_id);

This is also why bridgeParentStations is largely cosmetic.

Same-name station bridging

Many real-world feeds (e.g. France's Car Jaune) split a logical station into per-route platforms without linking them via parent_station. Stops named "Gare de St-Pierre" may exist as multiple distinct stop_ids a few metres apart, and raptor cannot transfer between them. bridgeSameNameStops: true synthesises walk transfers when names match and stops are within sameNameMaxMeters. Disabled by default because some feeds reuse names across distant locations.

Dates

raptor-journey-planner derives the YYYYMMDD date number with Date.toISOString().slice(0,10) (UTC) but uses Date.getDay() (local time) for day-of-week. Use a local-noon-UTC date such as new Date('2026-05-27T12:00:00Z') to keep both consistent across timezones.

time is seconds since midnight in service-day-local terms (e.g. 9 * 3600 for 09:00).

Performance

buildRaptorInputs reads each feed table once with a single ordered JOIN for trips ⨝ stop_times — no N+1 queries. On a typical 50k stop_times feed the build takes around 100 ms in Node + sql.js.

For browser use, run the whole pipeline (load + build + plan + hydrate) inside a Web Worker so the algorithm doesn't block the UI. Comlink works well; see the gtfs-sqljs README for the worker pattern.

Tests

npm install
npm test            # unit + e2e
npm run test:unit   # Google sample fixture only
npm run test:e2e    # Car Jaune fixture (Mairie de La Possession → Pyramide Fleurie 2026-05-27 09:00)

All tests run in Node with gtfs-sqljs/adapters/sql-js, which uses the same WASM/sql.js path that runs in browsers.

License

MIT (see top note about combined-license effects of raptor-journey-planner).