VIGO 0.5.2Open-source transit routing you can explain
Build a network. Answer a transport question.
VIGO 0.5.2 turns GTFS timetables and OpenStreetMap streets into a reusable City, then answers three questions about it: how to make a journey, how long every trip takes, and how far people can get. It runs on your own machine, explains each answer, and never needs an internet connection to route.
A Scenario changes the service one query sees, such as a new line or a removed route, so you can measure a proposal against today's network. Compare sets two saved Results side by side without routing again.
Choose your interface
Interface
Best for
What it needs
Engine (vigo)
Queries, resident streams, and the HTTP API
One native executable; no Node, Python, or network access
Compiler (vigo.mjs)
Building Cities and scenario collections; it can also query them
Node.js 24.18 or newer and its bundled native kernel
Studio
Inspecting service, planning journeys, and exploring Reach and scenarios
The desktop application and its own project library
Python (import vigo)
Studies, batches, notebooks, and journey tables
A platform wheel with a headless engine
The engine, the compiler, and Python open the same City directories. For the same City and request, the engine and the compiler return the same public JSON Result.
Built to be explained
Every answer carries its evidence. A Route Result says whether each walk follows mapped streets or a schematic station link. A Matrix keeps unreachable pairs null, never zero. A Reach surface records its grid so that two runs can be compared cell by cell. Known limits state plainly what the model does not establish.
Stable data. Cities built with VIGO 0.5 stay readable by later releases without rebuilding. See the prepared-data contract.
Stable results. The public vigo.route.v1, vigo.matrix.v1, and vigo.reach.v1 schemas only gain optional fields. See Read a Result.
Offline by default. Queries never fetch anything. This guide works offline too: press / to search, and open saved Results in the Result viewer.
What's new in 0.5.2
Route can return several journeys at once with alternatives. The engine and the compiler now return the same Result for the same request, including fares and walking connections. Studio saves journeys and comparisons as JSON, GeoJSON, CSV, or Markdown. Read the release notes and the changelog.
VIGO has four entry points. Most projects use two of them together: the compiler builds a City once from raw data, and the engine answers queries from that City for as long as you like.
You want to
Install
Requirements
Query a prepared City or serve HTTP
Engine archive (VIGO-Rust-…)
Nothing else at query time
Build Cities from GTFS and OSM
Compiler archive (CLI)
Node.js 24.18 or newer
Explore networks visually
VIGO Studio
The desktop application
Script analyses and notebooks
VIGO Python
Python and the wheel for your platform
Download archives from VIGO releases. Each archive targets one operating system and CPU; City data moves freely between the supported 64-bit targets.
Engine
Extract the engine archive and check the executable:
The executable contains the routing kernels, the City loader, the JSON interface, the HTTP server, and SQLite. It needs no compiler, Node, Python, browser, or internet connection to answer a query. The archive also contains this guide, the OpenAPI document, the engine manual, an audit record, licenses, and a manifest.json that hashes every file.
Compiler
The compiler archive contains vigo.mjs, its matching native routing kernel, license notices, and a checksum manifest. Keep vigo.mjs and vigo-routing-kernel.node in the same directory, then check it with Node.js 24.18 or newer:
The compiler builds Cities and scenario collections, and runs Route, Matrix, Reach, Stream, and Compare against them. It does not bundle Node or City data. The same steps ship inside the archive as its README.
Build from source
Use Node.js 24.18 or newer, npm 11.6 or newer, the Rust toolchain pinned in rust-toolchain.toml, and a C toolchain for the bundled SQLite. Build natively on the operating system and CPU that will run VIGO:
Shell
git clone https://github.com/hytangs/vigo.git
cd vigo
npm ci
npm run build:cli-runtime
npm run build:standalone
build:cli-runtime produces the compiler, public/vigo.mjs, and its native kernel. build:standalone produces the engine at native/vigo-routing-kernel/target/release/vigo. npm run build additionally compiles Studio, and npm run studio opens it.
Name the commands
The guides call the engine vigo and the compiler vigo-build. In a POSIX shell:
Shell
alias vigo="/absolute/path/to/vigo"
alias vigo-build='node "/absolute/path/to/vigo.mjs"'
From a source checkout, point them at native/vigo-routing-kernel/target/release/vigo and public/vigo.mjs. On Windows, use .\vigo.exe and node .\vigo.mjs, or define your own functions. Both commands accept --name value and --name=value and reject unknown options.
Studio and Python
VIGO Studio is the desktop workspace. It imports GTFS and OSM into its own project library and opens this guide offline under Settings → Documentation. See the Studio guide.
VIGO Python is versioned separately. Install the wheel for your platform, then follow your first Python query.
Supported platforms
macOS 13.5 or newer on Apple Silicon, Linux with glibc on ARM64 and x64, and Windows x64. Linux release builds use Ubuntu 24.04. There are no 32-bit or native Windows ARM64 binaries.
In about ten minutes you will build a City for Boston and Cambridge from public data, then plan a journey from Harvard Square to South Station, compute a small matrix, and map where you can reach in 45 minutes. A City is the prepared data directory, here ./boston.
You need the engine and the compiler, named vigo and vigo-build, plus curl, unzip, and Osmium Tool. The commands use a POSIX shell; on Windows, use WSL for the download and extract steps.
feed_start_date and feed_end_date give the published interval. The feed checked for this tutorial covers 2026-09-25 through 2026-12-12, and the examples use Monday 2026-10-05. With a later download, choose a date inside its interval and check calendar.txt and calendar_dates.txt: a date inside the interval does not mean every route runs that day. Times are local MBTA service times in America/New_York.
2. Build the City
Cut a smaller street extract around central Boston and Cambridge, then build:
The complete-ways strategy keeps every node of the ways crossing the rectangle. This extract covers the tutorial's places, not street access for the whole MBTA network; for a wider study, enlarge it or pass the full Massachusetts PBF. --prepare-date precompiles that day's timetable so that a fresh engine process can reuse it. Keep the providers' notices when you share data; see OpenStreetMap attribution.
VIGO writes one complete ./boston directory and leaves an existing output alone unless you pass --replace. vigo info confirms the City's name, revision, sources, and warnings. Keep the whole directory: the inspection output identifies the City but contains no routing data.
3. Plan a journey
Save route.json. Coordinates are [longitude, latitude]:
In a terminal, the first command prints a short itinerary. The second saves the full JSON Result. To ask when to leave to arrive by 09:00, override the clock and time direction:
Command-line flags override fields in the request file. In JSON, the same choice is "time": "09:00", "timePreference": "arrive_by", which both runtimes accept.
4. Read the Result
Read status first. ok contains a journey; not_found is a valid answer meaning no journey satisfies the date, walking limit, transfer cap, and time horizon you asked for. Then read journey.departureTime, journey.arrivalTime, and the ordered journey.legs. Clocks are local service-day HH:MM:SS; durations are integer seconds.
Trip IDs and times depend on the feed you downloaded, and this is scheduled routing without live delays. Check warnings and each walk's quality before treating a path as a passenger-facing guarantee; walking evidence explains station costs and schematic links. Open route-result.json in the Result viewer to see the journey as a timeline.
5. Compute a matrix
Save matrix.json. The rows are Harvard Square and Kendall Square; the columns are South Station and Copley Square:
Read durationsSeconds[origin][destination] in request order, and the matching journeys entry when requested. An unreachable pair is null, never zero. Repeated origins or destinations share work internally, and every requested cell is still returned.
6. Map where you can go
Save reach.json to find the streets and areas reachable from Harvard Square within 15, 30, and 45 minutes:
surface.valuesSeconds is a row-major raster from the northwest corner, and areas holds GeoJSON polygons for each cutoff. A Reach surface describes modeled travel time; it is not a count of jobs or people. See Reach for resolution, sampling, and comparison rules.
7. Keep the City loaded
One-off commands open the City every time. For repeated work, keep one process resident. Save queries.ndjson with one request per line:
Each output line echoes its id; check every status, because one failed line does not end the stream. To serve applications over HTTP, start the engine's service and call it from a second terminal:
Open http://127.0.0.1:8080/ to read this guide from the running service, and send requests from the API console at the end of the HTTP API page. When you measure performance, keep the City build, a fresh process's first answer, and requests to a resident process separate.
VIGO has four nouns and three questions. Once these are clear, every interface behaves the way you expect.
GTFS + OSM ──build──▶ City ──(optional Scenario)──▶ Route | Matrix | Reach ──▶ Result
City
A City is a compiled mobility model built from GTFS and OSM. It is one complete directory, so its timetable, streets, and prepared query data cannot drift apart. Rebuilding from new source data creates a new City revision; existing Scenarios and Results stay tied to the revision that produced them. revisionId identifies the immutable build, builtAt records when it was made, and sources records the GTFS and OSM inputs.
A built City reopens without its raw GTFS or OSM files. Building prepares the street indexes and station-access state. The first query for a new set of active services prepares a timetable snapshot that later processes can reload; dates with the same active services share it, and --prepare-date creates it ahead of time. A new process still loads the files into memory, while a resident process keeps them ready.
Prepared files are tied to the source revision, routing policy, and format. Missing, stale, or incompatible timetable and access caches are rebuilt from the compiled City without importing raw sources again. Required street files must remain complete: restore or rebuild a City with missing or corrupt street indexes. Updating the underlying GTFS or OSM data requires a new build.
Scenario
A Scenario is an immutable set of changes applied to one City revision. Planned service changes apply to Reach. Supplied traffic applies to Drive Route and Drive Matrix, and a supplied realtime snapshot applies to transit Route; requests carry these as scenario, traffic, and realtimeSnapshot. Walking limits, departure times, and cutoffs remain query options. A complete alternative GTFS source is a new City revision, not a Scenario.
VIGO rejects unsupported combinations before computing. It never silently drops a change or moves a Scenario to a newer City revision. See Scenarios.
Query
Query
What it computes
Notes
Route
Travel between ordered points, with legs, transfers, and walking
Depart-at or arrive-by; waypoints, alternatives, and departure windows are Route options
Matrix
Scalar travel time between sets of origins and destinations
A single origin is a one-row Matrix; journeys are optional
Reach
Where the network can take you within stated time limits
Shown as reached streets, areas, or contours
Depart-at minimizes arrival time, then boardings, then walking. Arrive-by maximizes departure time; among journeys leaving at that boundary and arriving by the deadline, it minimizes boardings, walking, and actual arrival.
Reach is not accessibility. An accessibility measure also needs opportunities such as jobs, people, schools, or health care, and a stated way to combine them.
Result
A Result is the immutable answer to one Query: status, values, warnings, timing, the City revision, the Scenario if any, and the query output. Compare is an action on two compatible Results, not a fourth query. Keep the original request and the full Result together; the normalized query echo is useful for inspection but is not a complete archive of every option or supplied observation. See Read a Result.
Choose a supported combination
Query
Travel mode
Time constraint
Supplied changes or observations
Route
Transit
Depart-at or arrive-by; waypoints and departure windows have their own limits
Matched GTFS-RT Trip Updates when realtime is explicitly selected
Route
Walk or Drive
Street routing with the chosen time direction
Supplied traffic for Drive in realtime mode
Matrix
Transit
Fixed departure or arrival deadline
Scheduled timetable only
Matrix
Walk or Drive
Static street metric; arrive-by does not introduce time-varying traffic
Supplied traffic for Drive in realtime mode
Reach
Transit with walking, or Walk
Fixed departure and time cutoffs
Planned-service Scenario; scheduled analysis
vigo capabilities describes the running build. Queries default to scheduled mode. Drive traffic needs both the traffic object and an explicit routingDataMode: "realtime"; VIGO never fetches a traffic provider. A visible vehicle, alert, or added trip is not itself a routing update.
Outcomes
Public Route and Reach Results use ok or not_found. A completed Matrix is ok, with null for unreachable cells.
Invalid or unsupported requests fail with an error and a nonzero exit code, or HTTP 4xx. Execution failures are errors too, never analysis Results.
Python Results use ready and blocked; those names belong to the Python API.
Studio background tasks report queued, running, ready, cancelled, or error.
Time
Service dates use the City's timetable timezone. A GTFS event at 25:10 belongs to the service date it was published on, even though it happens after midnight. Request clocks may exceed 24 hours, so always supply the exact service date with the clock. Realtime observation timestamps are a separate clock used only to admit or reject predictions.
VIGO keeps Build, Open, Compute, and End-to-end durations separate. Reusing an open process is useful runtime behavior; it does not hide or replace query computation.
These recipes start from the quickstart City, ./boston, and its coordinates. Each JSON block is a complete request to save under the filename shown. Replace the date with one your feed covers.
Choose a query
Question
Use
Read
How can someone make this journey?
Route
Timed legs, transfers, walking, and access qualifications
When must people leave to reach one place by a deadline?
Arrive-by Matrix
Per-pair duration and departure; optional actual journey arrival
Where can someone travel within a time budget?
Reach
Grid values and areas at the chosen cutoffs
How would a proposed service change that reach?
Baseline Reach, Scenario Reach, then Compare
Time changes and newly or no-longer-reachable cells
What are the live feeds reporting now?
Studio Network
Observation time, coverage, predictions, and source identity
The support table shows which modes and supplied states each query accepts.
Check status, then the chronological journey.legs. Walking competes with transit, so a short trip can return a walk; set requireTransitRide: true to require a boarding, or "mode": "walk" to request walking alone. A not_found Result can reflect the date, timetable, access network, or constraints. It does not mean the places are physically disconnected; Troubleshooting lists the checks in order.
For several journeys at the requested time, add "alternatives": 3. The default pareto ranking keeps only genuine trade-offs between arrival, boardings, and walking; "alternativeRanking": "ranked" lists the earliest distinct journeys even when one beats another in every respect. See alternative journeys and journey windows.
Every pair shares the deadline. durationsSeconds[origin][destination] runs from the latest feasible departure to the deadline, so it can include waiting at the destination; the matching journeys entry gives the actual modeled departure, arrival, walking, waiting, and transfers. Set includeJourneys to false when only times are needed. A shared destination and deadline share one reverse search, and a shared origin and departure share one forward search. Split larger jobs by shared time and options; see Matrix for limits.
The speed, headway, and operating span are illustrative. The example uses the defaults of bidirectional service, distance-estimated timing, and 0.35-minute dwell. It does not establish a drivable alignment, achievable running time, vehicle requirement, or service plan; Scenarios explains how to specify a real proposal.
Run both, then compare the saved Results with the compiler:
Compare uses the retained surfaces. counts splits cells reachable in both into faster, slower, and unchanged, and counts newly and no-longer-reachable cells separately; a negative meanChangeSeconds means faster modeled travel among the comparable cells. Read all the counts together. A larger area is not a population benefit: an accessibility study also needs opportunity data, a spatial assignment method, and an explicit measure. The engine's vigo compare takes both Results in one request; see Compare saved Reach results.
Investigate live service
In Studio, start in Network: check the observation time and reporting coverage, then choose a route in Routes. Compare scheduled and predicted events at the same stop, and inspect a vehicle's position separately from its stop-time predictions. With a connected model, the service briefing can interpret the retained evidence; its explanation remains subject to review.
Use Route → Realtime to query supported, matched predictions. A vehicle on the map, an alert, or an added trip in Trip times does not by itself make that service available to routing. See Network and Routes and realtime admission.
For a reproducible analysis, use an exact date and time in Scheduled mode. For an incident review, retain the observation, identity, coverage, and Result from the original investigation. Read a Result covers both.
A City is compiled once by vigo-build and then opened read-only by any number of engine processes, Python sessions, or compiler queries. This chapter covers inputs, build options, and the life cycle of a City. The quickstart shows a complete Boston build.
Choose inputs
Input
Requirement
GTFS
One or more static GTFS ZIP files. Repeat --gtfs for several feeds and give each a stable --gtfs-scope; see multiple feeds.
OSM
One .osm.pbf covering every origin, destination, stop access, and transfer location you will query.
Keep the original files, their checksums, and their acquisition date. Download URLs such as latest are not frozen datasets. GTFS support lists the source features VIGO reads and the ones it reports as unsupported.
Reuse identical immutable streets from a prepared City
--replace
Replace an existing output; without it, an existing City is left alone
Build is staged: a failed build preserves the previous City, and --replace restores the previous directory if publication fails. A successful replacement removes its temporary backup, so use a separate output directory when you need a rollback copy.
Raw import and street preparation happen at build time and are separate from query latency.
Large regions and low memory
Builds use disk-backed SQLite temporary work and compact numeric node lookups, report progress during long import phases, and respect container memory limits when deciding whether GTFS and OSM compilation may overlap. Import and native street preparation run in separate processes, so each stage releases its working memory before the next. Keep the output and temporary directory on a local filesystem (inside WSL, prefer the Linux filesystem to /mnt/c), and leave space for the source database, temporary sorts, and the finished City.
Regional native CCH preprocessing can still exceed 2 GiB without swap. Configure disk-backed swap for the build machine, or build on a larger machine and copy the complete City to the server: build memory and query memory are separate requirements. Street preparation must write the walking accelerator before the graph is sealed, so a City that could not open is never published.
Copy and load a City
A City contains network.json, routing/project.sqlite, prepared access artifacts, and an osm directory with street snapshots, access profiles, and CCH indexes. Exact member names can vary with the preparation version. Copy the whole directory, never just the SQLite files or the manifest.
The engine checks source identities, snapshot formats, station and access profiles, and required CCH members. It rejects blocking source features, missing or stale preparations, and an unexpected SQLite WAL. Complete a City through its compiler; do not delete WAL files to bypass validation. The engine reports timing.timetableSource: "prepared_snapshot" when it reuses a prepared timetable and "source" when it prepared one from SQLite for that process. Both use the same schedule.
Update a network
Treat a published City as immutable while any process has it open. Compile updates into a new directory, start another service against it, verify readiness and a known query, switch traffic, and then stop the old service. The engine has no upload endpoint, source downloader, or hot reload. See Upgrade and rollback for version changes.
Scenario collections
When you compare several timetables over one street network, build a scenario collection: one shared street graph, several transit scenarios, and a default. See Shared scenario collections for the source manifest and residency rules.
A City can contain several static GTFS feeds and several GTFS-Realtime endpoints. Static feeds supply the scheduled network; live feeds supply observations and predictions for it and never replace the timetable.
Build a City from multiple GTFS files
In Studio, open City → Data sources and add each GTFS ZIP. VIGO imports each source, then builds the combined routing store. Add an overlapping OSM PBF for walking, driving, and coordinate-based transit access, and wait for preparation to finish before querying.
With the compiler, repeat --gtfs and give each input a stable, unique scope:
A scope identifies a source feed, not a route number. The merged store namespaces stops, routes, trips, services, and transfer references so identical IDs in different feeds cannot overwrite one another. Use the same scope names when you rebuild the same sources. Removing a source in Studio rebuilds the surviving combined store.
Before combining feeds:
Use compatible service dates. A trip is usable only when its own calendar and exceptions activate it for the requested service day.
Keep agencies in one timezone. Cross-timezone routing stores are rejected.
Avoid importing two editions of the same agency timetable as separate networks unless that duplication is intentional.
Retain declared transfers and verify street coverage. Close coordinates alone do not prove a usable station connection.
Connect multiple GTFS-RT endpoints
Open City → Data sources → GTFS-RT live feeds, or feed settings in Network.
Paste an endpoint URL and choose what it contains. A combined endpoint may contain several entity types.
Select its matching Timetable when the City has several static sources.
Choose Add feed for each additional endpoint, then Connect live.
Expand Feed status to inspect individual failures and freshness.
Live-feed requests use { "sources": [{ "url": "https://operator.example/trip-updates.pb", "kind": "tripUpdates" }] }. The old top-level url and urls formats are rejected; reconnect those saved connections through the feed editor. A single MBTA endpoint no longer implicitly enables the other two.
Trip Updates can be connected without Vehicle Positions. The MBTA preset fills three public endpoints; select the MBTA timetable when you use it in a combined City. A failed endpoint does not discard successful ones. Reimporting or merging a timetable invalidates the retained observation until a fresh fetch binds to the new City, and old records from a failed endpoint are not carried into a new snapshot.
Data
What VIGO uses it for
Trip Updates
Supported predictions and cancellations in transit Route; trip and station inspection
Vehicle Positions
Reported map locations, vehicle details, and qualified service observations
Alerts
Scoped notices and evidence; text does not automatically close a route
Studio allows up to 16 distinct endpoints. Repeated URLs for the same timetable are fetched once, and assigning one URL to conflicting timetables is rejected. A refresh has a 30-second overall deadline, a 20 MB per-feed limit, and a 40 MB combined limit. Batches also reject more than 200,000 entities or 500,000 stop predictions. At most four downloads run concurrently, and oversized or failed sources are disclosed individually.
The declared type is descriptive: every supported entity present in the protobuf is decoded. A source timestamp and timetable identity travel with each record. Unscoped IDs match only when unambiguous; a bare trip ID never resolves by choosing the first agency.
Supply observations to the engine
Neither command-line runtime opens feed URLs. A Route request can carry a retained, normalized realtimeSnapshot with routingDataMode: "realtime". In a combined City, each record can carry sourceScope equal to its build scope, alongside the original tripId, sourceFeedTimestamp, service date, and stop predictions. A single-source City uses raw GTFS IDs; omit sourceScope there. The realtime input reference lists every record field.
Keep source timestamps intact. Changing a capture's timestamps to make it look current changes the input and is not a replay of that observation. A repeated snapshot ages out of the routing cache.
A collection serves several transit scenarios against one immutable street graph. Prepare it with node public/vigo.mjs build-scenarios --spec scenarios.source.json --output scenario-set, then point vigo serve --city scenario-set or vigo stream --city scenario-set at the result. Each request selects a scenarioId; omission selects defaultScenario. The one-shot CLI also accepts --scenario ID.
For wheelchair routing, add "wheelchair": true to the source specification or pass build-scenarios --wheelchair. Every member and the shared street graph then use wheelchair-strict-v1; unknown accessibility is excluded. Wheelchair collections require streetModes: "walk". Conflicting CLI/spec values and non-boolean specification values are rejected before compilation. Use a separate regular collection when both profiles are needed.
Source paths are relative to the manifest. Optional sha256 values pin each input. The compiler prepares street data once and creates relative references to it. Move the collection as one directory. Its checksum inventory records physical files once. prepareDates precompiles up to 32 service dates, reusing identical service sets; other dates remain queryable through the source timetable. A single-City build accepts repeated --prepare-date options.
The Engine CLI and HTTP service use that City for Route, Matrix and Reach. Add "wheelchair": true to the request, or --wheelchair to an Engine CLI query. Omitting the option inherits the City's profile. Ordinary Cities reject a true request; wheelchair Cities reject false, Drive, realtime transit and query-time scenario overlays. Compile changed inputs into a new wheelchair City. Serve the prepared wheelchair City using the same serve --city wheelchair-city command and authenticated /v1/route, /v1/matrix and /v1/reach endpoints.
The wheelchair-strict-v1 profile requires GTFS wheelchair_accessible=1 for vehicles and wheelchair_boarding=1 for boarding/alighting, including published parent inheritance. An accessible vehicle can pass an inaccessible stop without boarding or alighting. Unknown values are excluded. Search considers accessible alternatives directly, including later vehicles; it does not filter a completed unrestricted journey.
OSM ways require wheelchair=yes and ordinary pedestrian permission. Steps, escalators, mapped inaccessible or limited nodes, raised kerbs, unverified barriers and conditional accessibility are excluded. Published widths below 0.9 m and inclines above 8.3% are excluded. Untagged shape vertices inherit the way's accessibility. These are fixed profile limits, not a personalized chair model.
Station paths require either a positive published wheelchair_traversal_time (the MBTA extension) or step-free mode with published width at least 0.9 m, slope at most 0.083 in magnitude and a traversal cost. Elevator paths do not require slope. Explicit stairs and escalators remain excluded even if a contradictory wheelchair time is present. Direction is preserved. Missing paths do not generate parent-station shortcuts. Generic transfer recommendations do not establish accessible paths. For transit access, station platforms are reached from declared accessible entrances and paths, rather than snapped directly from the street.
Every successful or no-route response carries accessibility, including the profile and its limitations. Coordinate-to-network attachments are geometric; door-to-street access is not verified. Live elevator outages, temporary barriers and individual chair dimensions are not modeled. Strict source requirements can leave sparse coverage or no route. A no-route result is not evidence that no accessible real-world trip exists.
Source semantics: GTFS Schedule, MBTA pathways, and OSM wheelchair tags. The public synthetic regression is npm run check:wheelchair; it checks accessible alternatives through both CLIs and authenticated HTTP, as well as Matrix, Reach, reverse routing and profile mismatch rejection.
Route finds and explains travel between ordered points. This chapter describes how Route behaves; the Route request reference lists every field and default.
Inputs
A Route query selects:
an origin, optional ordered waypoints, and a destination;
transit, walk, or drive;
an exact service date and local time;
depart-at or arrive-by;
a walking limit, an optional maximum number of transfers, and the objective;
optional alternatives or a departure window;
optional supplied observations supported by the selected mode.
Stop IDs are exact GTFS identifiers. Coordinate points are [longitude, latitude] and need streets in the City.
Walking and transit
Transit Route and Matrix compare a direct OSM walk by default, so a short coordinate trip can return walking when it arrives sooner. Set requireTransitRide: true to require at least one boarding, or select Walk mode to request walking alone. maxWalkKm limits each access and egress walk. Set allowLongWalk: false to apply the same limit to a direct walking alternative; otherwise maxStreetKm bounds the complete walk.
horizonMinutes sets the timetable search horizon (default 480, range 1–2880). For depart-at transit, boarding and alighting must happen at or before this boundary, including in alternative journeys; the final walk can finish after it. The horizon is therefore not a hard limit on door-to-door duration: comparisons with a total-duration cap must check the final arrival separately.
allowStreetTransfers (default true) controls walking connections between separate stops or stations. Turn it off to change vehicles only at the same stop or between members of one station, as identified by the GTFS station hierarchy. The engine removes cross-station walking edges before searching, including for arrive-by, realtime, and departure-window searches; access to the first stop and egress from the last remain available. Studio exposes this as Route options → Allow walks between stations or stops. It does not certify station-interior paths or free-transfer eligibility.
disableCache: true disables street-access frontier and walking-path caches while keeping the prepared City resident. Route answers are recomputed either way. The same flag applies to the wider access probes used to explain a blocked Route.
Transfers and time reserves
maxTransfers accepts integers from 0 to 31: 0 permits one boarding and 1 permits two. Omit it for no additional cap. Staying aboard the same trip is not a transfer. The cap constrains the native search, including alternatives and arrive-by, so a slower feasible route is found when the unrestricted winner exceeds it. A finite cap with ordered transit waypoints is unsupported.
minimumTransferBufferMinutes (integer 0–60, default 0) adds time before each subsequent boarding, after any transfer walk and the feed's own minimum transfer time. It is charged once per transfer, including a change at the same stop, and never delays the first boarding, the final egress, or staying aboard. arrivalBufferMinutes plans an arrive-by query against an earlier deadline. Both are timetable feasibility constraints, not delay predictions; see Time reserves.
The engine adds no implicit boarding buffer. Same-stop changes honor published GTFS minimum transfer times and forbidden transfers. Explicit transfer edges keep their durations without an added margin or a 60-second floor, and a published platform-to-platform transfer rule takes precedence over the station walking fallback.
How a journey is chosen
VIGO's objective is earliest_arrival. Equal-arrival journeys prefer fewer boardings, then less walking, then a stable final order; there is no undefined "balanced" preference. Arrive-by first maximizes departure time; among journeys leaving at that boundary and arriving by the deadline, it minimizes boardings, then walking, then actual arrival. A slightly later on-time arrival can therefore avoid unnecessary transfers.
If the reverse boundary cannot be materialized at first, the engine reconstructs a forward witness at the same departure from the complete access frontier. A remaining mismatch is a query error; an earlier feasible departure is never reported as the latest. An independently verified direct walk may dominate the reverse transit bound.
Ordered transit routes can walk any segment, including the whole journey. Selection compares complete journeys by arrival (or latest departure), then boardings and walking, so a later walk to a waypoint can replace a short ride when both catch the same onward vehicle. Up to eight partial journeys are retained at each waypoint and up to five complete choices are returned; this bounded search does not enumerate every itinerary. requireTransitRide: true requires a ride on every segment.
Alternatives and windows
alternatives (0–9) asks a depart-at transit Route for more journeys at the requested time; both runtimes return the same list. A departure window instead samples nearby departures: the engine's windowMinutes searches later departures or earlier deadlines, while the compiler's departureWindowMinutes (±0–30) is a centered depart-at profile. Sampled windows are not continuous timetable profiles. See Alternatives, via points, and departure windows.
Result and diagnostics
The public vigo.route.v1 Result contains status, a journey with chronological legs and durations in seconds, warnings, quality, and meta. A not_found Result is a completed answer with journey: null and an access reason when one is known. Read a Result defines every public field.
For a detailed diagnosis, request diagnostics: "trace" and read the internal result under trace. Access failures carry endpoint-specific accessAvailability:
outside_selected_budget: an access candidate exists at a larger walking limit. The suggested limit does not guarantee a complete itinerary; streetPathVerified records whether its path was verified.
street_access_unverified: nearby stops exist, but the bounded street search did not verify access. nearestStop.distanceKind: "straight_line" describes proximity, not walking distance.
none_within_probe: no access candidate was found within probeWalkKm. This is a bounded result, not proof of disconnection at every distance.
diagnostic_unavailable: the explanatory probe failed, reported as access_diagnostic_unavailable.
The diagnosis never raises the walking allowance by itself. Blocked plans keep searchLimits for walking, horizon, transfers, and the transit-ride requirement, including horizonScope: "timetable_scan". no_path means no scheduled itinerary under those constraints on the selected date; changing a constraint requires a new query.
Stations, access, and geometry
Anonymous coordinate endpoints and physical GTFS stops use the same nearest-street attachment in Route and Matrix. A parent station does not provide free movement between all its platforms. Declared station paths keep their direction and time; their complete time and distance frontier is prepared once and then filtered against each endpoint's remaining walking budget. Interior pathway nodes do not create extra street entrances.
When GTFS omits traversal_time, the walking policy prices the declared pathway length, or the distance between stop coordinates if length is also absent. Stairs with a stair count and fare or exit gates use labeled estimates described in walking evidence, floored by any available walking distance. A missing time is never a zero-time link. Endpoint walks that use a station path expose accessCost.street and accessCost.station separately, including directed stopIds and source types; this supports a source-data audit, not certification of physical station access.
The walking limit covers each complete, continuous access or egress walk; a transfer walk cannot extend the final egress beyond that budget. Street transfers use declared entrances and directed station pathways, composed around at most one external transfer edge. A repeated station in a ride sequence is reported but not rejected, because a scheduled loop or a forbidden platform change can require it.
A coordinate that snaps to the street graph can carry an endpointConnector on its first or last walking leg. Its coordinates and distance describe the snap already included in the leg's cost, with source: "coordinate-snap" and streetPathVerified: false. A connector is not evidence of a mapped or legally traversable street. For geometry materialization and identifier internals, see itinerary geometry.
Matrix calculates travel time between sets of origins and destinations in one request, from a shared departure time or to a common arrival deadline. Request journeys only when you need their steps. The Matrix request reference lists every field.
Replace A and B with exact stop IDs in your City, or use "coordinate": [lon, lat] points; Walk and Drive matrices require coordinates. The quickstart has a complete four-pair Boston example.
Read the result
durationsSeconds[originIndex][destinationIndex] gives each pair's elapsed time, and the ordered endpoints are echoed in query. A null cell means unreachable, never a zero-minute journey; a completed matrix can contain both. With includeJourneys: true, journeys has the same shape with a journey or null per pair. Add includeGeometry: true for path geometry; it requires journeys and performs no extra timetable search.
For arrive-by, a scalar duration is the deadline minus the latest feasible departure. A nested journey reports its actual arrival and may be shorter if it arrives early. Use the journey to separate travel from waiting at the destination. Public durations are seconds; minute-valued rows and search counters appear only in explicitly requested trace output.
Choose constraints
The Route rules apply: a feasible direct walk competes unless requireTransitRide: true, maxWalkKm bounds endpoint walking, maxTransfers accepts 0–31, and horizonMinutes bounds the timetable search rather than the door-to-door duration. allowStreetTransfers, minimumTransferBufferMinutes, and arrivalBufferMinutes behave as described under Route.
Transit Matrix is scheduled. Drive Matrix can use a supplied traffic snapshot with routingDataMode: "realtime"; it never fetches a provider or models traffic changing during a journey. See Street routing.
Share work across pairs
Results keep input order, including repeated endpoints. The compiler accepts up to 100,000 pairs in a 16 MiB request; the engine accepts up to 65,536 pairs in an 8 MiB request. Group work by shared endpoint, date, clock, and constraints:
Workload
Shared timetable search
Depart-at, one origin and many destinations
One forward scan
Depart-at, many origins
One forward scan per unique origin
Arrive-by, many origins and one destination
One reverse scan
Arrive-by, many destinations
One reverse scan per unique destination
Request scalar output when only times are needed. Journey queries also share boarding rounds within each endpoint group, so tied journeys can differ from Route's stable order while satisfying the same objectives. disableCache: true bypasses endpoint-frontier caches while keeping same-request sharing, loaded City data, and immutable street indexes; it does not clear the operating system's page cache. Service benchmarking shows how to measure full caller wall time.
Reach finds the streets and areas reachable from one origin within a time budget, by scheduled transit with walking or by walking alone. It does not measure access to jobs or people unless your analysis adds those opportunities. The Reach request reference lists every field.
mode: "walk" requests walking-only Reach and needs no service date. Drive, arrive-by, and realtime Reach are unavailable. Planned transit changes use transit with street sampling.
Set the analysis resolution
Option
Meaning
cutoffsMinutes
Time budgets from 1 to 240 minutes; fractional values are accepted
maxWalkKm
Transit walking budget
walkSpeedKph
Walking speed from 1 to 8 km/h
extentRadiusKm
Requested raster extent from 1 to 40 km
rasterSize
Cells per side; the compiler accepts 48, 64, 96, 128, 192, 256, 384, 512, or 1024, and the engine any integer from 16 to 1024
surfaceSampling
street samples reached street intervals; cell-center routes to each cell's center
The extent sets the analytical grid; it is not a maximum trip distance. Time budgets, walking limits, and the network decide what is reachable. Keep extent, resolution, sampling, and constraints identical when comparing results.
Choose the output
The default reachFormat: "full" returns the requested grid and the full reached extent. surface.valuesSeconds is row-major from the northwest corner, and null means unreached; read its bounds and dimensions with the values. fullSurface describes the separate full extent when available.
areas are GeoJSON polygons around reachable cells, preserving holes and islands. contours interpolate the sampled times and close at the grid edge and no-data boundaries; diagonal-only cells remain disconnected. The raw raster stays the reference for numerical analysis. For a map, request reachFormat: "map" to receive only the full reached-area polygons and their bounds, without raster values, stop lists, or duplicate contours; map output cannot be compared.
For network evidence, add includeStreetEdges: true (--street-edges on the command line) to the full output. Returned directed intervals keep their explicitly named units. See Reach output for raster ordering and edge decoding.
Compare with R5 and OTP
Use surfaceSampling: "cell-center" for a coordinate-destination surface. The default street mode records the earliest reached street portion in each cell and leaves cells away from reached edges empty; a filled street cell does not establish a route to its center. Area metrics sum sampled cells and do not measure physically walkable land.
When comparing engines, match the origin, service date, departure time, modes, walking and transfer rules, cutoff, extent, and grid resolution before drawing polygons. VIGO Reach evaluates a fixed departure. Conveyal's R5 can aggregate a departure window at a chosen percentile, and OTP's Travel Time API provides a departure-time snapshot; match the temporal statistic first. VIGO raster values are seconds, while requests use cutoffsMinutes.
For quantitative agreement, compute travel times at the same coordinates with Matrix, then classify them at the same cutoff. A longitude/latitude grid and a Web Mercator grid do not share cell centers. Compare contours separately from cell polygons, and check station entrance costs, boarding slack, and GTFS transfer minimums in representative journeys. For a Transit Matrix control that matches Reach's walking budget, set requireTransitRide: false and allowLongWalk: false. See Conveyal's isochrone methodology, OTP's Travel Time API, and the GTFS minimum-transfer rules.
Inspect a destination in Studio
After running Reach in Analyze, click a destination to see its map estimate and request a journey there; with the map focused, press Enter to inspect its center. The journey keeps the analysis origin, date, departure time, timetable selection, and walking and ride limits. Expand Show journey for the walking and transit legs. A missing raster sample does not by itself mean no journey exists.
Destination journeys use scheduled service and the City's routing walking speed. Planned service changes affect the scenario map estimate but are not reconstructed in the destination journey; the panel labels the difference.
Compare service changes
Run a baseline, then a planned Scenario with the same origin, clock, date, walking assumptions, sampling, and grid, and retain both Results. Scenarios explains exclusions and added service; Compare saved results explains the counts. A polygon describes modeled network reach. It does not certify entrance access, wheelchair connectivity, pedestrian permissions, or observed reliability; keep the walking evidence and known limits with the analysis.
A Scenario is an immutable set of changes tied to exactly one City revision. It may contain planned transit service changes, one live transit state, or one supplied traffic state. Walking limits, speeds, departure times, and cutoffs stay query options.
Scenarios never edit the imported GTFS or OSM, and a complete alternative feed is another City revision. Drawing geometry in an editor creates no computational change until it is attached to a valid service edit. Several nonconflicting planned changes may coexist; conflicts need an explicit resolution. A Scenario never moves to another City revision, and expired live state cannot be queried.
In VIGO 0.5.2, planned service changes apply to Reach, supplied traffic to Drive Route and Drive Matrix, and supplied realtime predictions to transit Route. Transit Matrix and Reach remain scheduled. Unsupported combinations are rejected with an explanation. The Scenario request reference lists every field.
Planned service
A planned service change states what changes and supplies enough to run it: ordered stops, travel-time assumptions, and a departure schedule. Edits to existing branches keep their GTFS trips by default; new service specifies frequency and operating hours. A Reach request can include this scenario:
operation is add, augment, or replace. excludedRouteIds removes selected scheduled route variants for the Scenario.
Replace a branch and keep its trips
replace defaults to scheduleMode: "preserve-trips" and needs sourceRouteId, sourcePatternId, and selected-branch scope (routeScope: "pattern", or omitted when a pattern is supplied). Studio and the compiler load the trips active on the query's service date, including calendar exceptions and departures after midnight. Each affected trip produces exactly one replacement, keeping irregular gaps, per-trip running times, dwell, and pickup and drop-off restrictions. Editing stops never adds departures or reverse service. Older replacement requests use the same default: headway and hour fields are ignored unless frequency mode is selected explicitly.
For a branch that keeps its trips, road geometry distributes each trip's original A → B running time among the edited gaps. Original dwell remains at retained stops, and addedStopDwellMinutes (default 0.35) adds dwell at inserted stops; removing an intermediate stop removes its dwell. The first retained GTFS stop anchors departure timing: a preceding extension departs earlier, and later edits shift downstream times. At least one original stop must remain, with baselineStopId or baselineStopIndex identifying moved stops and repeated visits. An extension requiring a departure before the service date is rejected. These are modeled timing changes, not a vehicle or crew scheduling check.
Set a frequency instead
To change the timetable deliberately, use scheduleMode: "frequency" with headwayMinutes, startMinutes, and endMinutes; Studio calls this Departures → Set frequency and hours. This mode can replace a whole route, and add and augment use it by default. New lines use segmentDistancesKm and averageSpeedKph for road timing, plus dwellMinutes; segment arrays have one value per stop pair. Reverse service is an explicit choice in frequency mode and is never inferred from a shared route name.
Edit a shared edge in Studio
Selected branch only keeps a change on the chosen pattern. All branches serving this exact A → B edge applies one inserted gap to every occurrence of that ordered stop pair within the same feed and public route, but not to the reverse B → A edge. The editor lists the affected branches before running. This scope needs complete branch analysis and one inserted gap; use selected-branch scope for moved stops, extensions, or several gaps. Repeated stop visits keep their own positions and published runtimes, and overlapping replacement services are rejected. Studio requires a completed road path before running a road-following Scenario.
Compare the change
Run a baseline Reach and a Scenario Reach against the same City, origin, date and time, walking policy, cutoffs, and grid, save both, and compare them. Test a planned service change has complete paired requests. Compare reports after-minus-before changes for cells reachable in both surfaces and counts newly and no-longer-reachable cells separately. It checks grid compatibility but not every experiment setting, so read Compare semantics before attributing a mean change to the Scenario.
Route can use supplied GTFS-Realtime Trip Updates. This chapter explains how predictions are admitted and how to read the outcome; the realtime input reference lists the snapshot fields.
Two modes, one engine
Route exposes routingDataMode: "realtime" | "scheduled", shown in Studio as Realtime and Scheduled · Research. The command-line runtimes default to scheduled and use a supplied realtimeSnapshot; they never fetch feeds. The compiler also accepts --data-mode realtime|scheduled.
Scheduled research requires an explicit service date and time. The server removes realtime and traffic observations before processing, enforces the exact date, and disables date substitution. Studio keeps the selected date and time and ignores feed polling in this mode; live refreshes, expiry checks, and mode changes cancel obsolete realtime requests, and switching modes never shows the other mode's old result.
Both transit modes use the same native algorithms, stop identities, walking and transfer policies, and coordinate search; only the timetable presented to the engine differs. Transit Matrix and Reach remain scheduled and reject realtime requests.
Results for an explicit mode include diagnostics.routingDataMode and routingDataProvenance: source timetable and street identity, service date and timezone, query time, walking and search settings, engine contract version, and a reproducibility key. Realtime results also identify the prediction snapshot, and ordered journeys reject changed data identities between legs. The manifest identifies inputs but does not archive them; retain the City, the VIGO build, the request, and the returned diagnostics to reproduce a result. The 0.4.2 method audit records earlier correctness repairs.
Shared timetable
A journey search compiles the complete supplied TripUpdate snapshot into a separate resident timetable. Within the explicit resource limits there is no endpoint-based selection, inspection pruning, or duplicate-stop overlay, and an oversized snapshot is rejected as a whole rather than truncated. Each valid matched update replaces its scheduled trip; canceled and deleted trips lose their ride segments; unreported trips keep scheduled times.
Depart-at, arrive-by, transfer limits, and departure-window alternatives all use this timetable with the existing native algorithms. A query freezes its observation clock, including recursive verification and window samples. The latest compiled snapshot is cached per immutable service kernel and invalidated when input or timestamp validity changes; scheduled queries never inherit predictions from an earlier request.
Admission and coverage
An update must identify an active trip on the selected service day and source scope. Ambiguous identities, contradictory duplicates, invalid timing, unsupported relationships, and stale observations are excluded and counted. Feed and record timestamps are checked independently: admission accepts timestamps at most 180 seconds old and at most 60 seconds ahead of the captured clock. Network tools may apply their own freshness policy first, keeping all rejection counts.
An unreported scheduled prefix may conflict with the first explicit prediction of an early-running trip. The resolver excludes that prefix only when all its departures and the first explicit prediction precede the frozen observation boundary, with no earlier explicit update or trip-level delay. pastPrefixTrips and omittedPastPrefixStops disclose the exclusion; it does not reconstruct past events or claim actual passage.
diagnostics.realtimeRouting identifies the snapshot and counts applied, canceled, stale, unmatched, wrong-date, duplicate, invalid, and unsupported records. coverage accounts for every supplied record, including those rejected before routing. partial means some updates were applied while others were excluded, and failedFeeds reports unavailable endpoints. A scheduled fallback is identified explicitly; compile or search failures never silently retry against scheduled service.
Processing a full snapshot is not a claim that every trip has a prediction. Added, unscheduled, replacement, and duplicated trips without a supported scheduled instance remain unsupported and disclosed. Vehicle positions and text alerts never invent stop-time predictions. Studio's added-service display is separate from routing admission.
Read the realtime status
For transit Route, read diagnostics.realtimeRouting.status together with its counts. routingDataMode: "realtime" records the requested mode; it does not mean that predictions changed the timetable.
Status
Meaning
applied
Matched trip replacements were applied, possibly with cancellations, and no supplied update was rejected
cancellations_only
Only trip removals were applied, and no supplied update was rejected
partial
At least one replacement or cancellation was applied and at least one supplied update was rejected
stale_fallback
No update was applied and feed or record timestamp checks failed
no_matches
No update was applied for other reasons; check unmatched, wrong-date, duplicate, invalid, and unsupported counts
scheduled_fallback
No realtime search diagnostics were available; read fallbackReason
coverage.complete concerns admission of the supplied updates; it does not measure the share of scheduled trips that report. A fully admitted cancellation snapshot can legitimately return no journey. routingDataProvenance.realtimeApplied counts both replacements and cancellations. Keep these fields with the answer; see Keep uncertainty with the answer.
Stop predictions and source versions
A trip-level delay propagates forward until a stop-specific prediction replaces it, and a prediction at an intermediate stop never becomes a delay for earlier calls. Absolute time takes precedence over delay; arrival and departure remain separate. NO_DATA resets propagated delay, and SKIPPED removes boarding and alighting while keeping through travel. Repeated stops need an unambiguous sequence, and out-of-order sequences are rejected.
Each decoded record keeps its endpoint, source timestamp, and static-feed scope. VIGO does not compare GTFS-RT feed_version with static feed_info.feed_version, so connect the matching timetable edition yourself. A missing Trip Update means no reported prediction, not verified on-time operation.
Resource admission is separate from semantic coverage. Downloads are bounded, the decoder rejects feeds above 100,000 entities or 500,000 stop predictions, and native reconstruction checks its estimated memory against the timetable budget. Over-limit data is rejected, never silently truncated.
Verification
npm run check:gtfs runs the full feed and routing suite, including more than 256 updated trips, 1,024 input records, and 4,096 updated stops, interior transfers, both time directions, cancellations, skipped stops, transfer limits, snapshot replacement, and timestamp expiry. check-realtime-scheduled-parity.mjs compares 1,536 paired engine queries against eight independently imported GTFS timetables containing the literal effective predictions. check-realtime-past-prefix.mjs and check-routing-data-modes.mjs cover excluded prefixes and observation isolation. These establish algorithm parity for the tested cases, not field ETA accuracy.
A City built with OSM supports Walk and Drive variants of Route and Matrix, plus directed walking access for transit. Walk uses the directed pedestrian graph; Drive uses the directed road graph with free-flow time unless the request supplies traffic weights or closures. VIGO never replaces a missing street path with a straight line.
Street Route needs coordinate endpoints and returns full geometry. Street Matrix returns scalar distance and duration per pair without reconstructing every path; it sweeps from the smaller endpoint set, and many-to-one requests use the reverse directed CCH metric while keeping each endpoint's access permissions.
Coordinate network attachment
A free coordinate first attaches to its nearest pedestrian vertex or reciprocal edge. If that attachment belongs to a component with no public transit-access anchor, the query can recover to the nearest anchored component within 80 m. This applies to arbitrary coordinates without place-name exceptions. The connector and partial-edge distance count toward walking time and limits, and returned geometry includes the charged connector. Anonymous Reach seeds use the same recovery.
Stops keep their prepared physical anchors. Recovery never adds a street edge or a stop-to-stop transfer between disconnected components, a mapped authorized terminal path takes precedence, and a restricted endpoint cannot use recovery to bypass its directed exit. An off-network connector is a modeling assumption, not evidence of a surveyed entrance or a barrier-free crossing; an isolated place with no eligible street inside the radius stays unreachable.
Authorized endpoint access
Builds default to public pedestrian access: access=private without a pedestrian permission, or foot=private, stays outside the public walking graph. For a population authorized to use internal roads at its own homes and destinations, build with --private-access=endpoints. In Studio, choose City → Street network → Pedestrian access → Authorized endpoints before adding or replacing the OSM PBF; choosing Public paths only and replacing the streets removes the overlay.
This City-wide modeling assumption permits mapped private streets only within the origin's or destination's attached internal street region. An exact directed search follows those streets and interior public islands up to the first public component with a transit-access anchor. The public middle of the journey and stop-to-stop transfers never enter private streets, all internal walking counts toward the same walking limit, and geometry follows the mapped edges. Walking wholly within one internal region is supported.
The private graph is stored separately, so public street queries are unaffected. network.json records the access model, and transit diagnostics report walkingAccessPermission: "authorized_endpoints". The option assumes endpoint authorization; it does not infer parcel ownership, gate hours, or missing OSM links. Explicit pedestrian prohibitions stay excluded, gates marked prohibited, locked, or with unresolved opening times gain no connections, and Street Reach follows the same rules at the origin and at reached destinations. Rebuild older Cities to apply street-store schema v6, which also excludes platform area outlines from the linear walking graph.
Supplied traffic
No traffic provider is connected by default. OSM supplies the baseline road graph and free-flow estimates, and live transit feeds are not a road-speed model. Driving results without an applied traffic snapshot must not be described as current traffic estimates.
Drive Route and Drive Matrix accept a top-level traffic object with routingDataMode: "realtime". A snapshot customizes the prepared directed road graph for one query: a fixed set of edge costs, not a forecast that evolves as a vehicle moves.
Use the actual observation time and the coordinates of an observed directed edge. This fragment shows the schema; its values are illustrative. A full request also needs the mode and endpoints.
Contract
Behavior
Time
Required observation time; validity defaults to 300 seconds and is at most 1,800; observations more than 60 seconds in the future are rejected
Expiry
Expired observations produce an explicit stale_fallback and use baseline costs
Geometry
Endpoint pairs or 2–512 coordinates; directed edge matching within 120 m by default, at most 1,000 m
Edge indices
Require the exact current streetSourceFingerprint; indices are not portable between street builds
Costs
Travel time, speed, or a slowdown factor, plus closures; costs never become faster than the baseline
Overlap
Conflicting observations on one edge use the largest cost, including closures
Reuse
The customized metric is reused while effective edge updates are unchanged; source age is checked on every query
Read diagnostics.traffic.status, the matched and unmatched counts, and weightModel. Only applied establishes that supplied costs changed the model; no_matches and free_flow_equivalent do not establish current coverage. Coordinate matching is nearest-edge matching, not a validated provider conflation pipeline, and VIGO does not yet model every turn, signal, intersection delay, parking, or a future traffic trajectory. The traffic input reference lists every effect and limit.
VIGO supports explicit time reserves that change which journey is chosen. It does not yet estimate an on-time probability, a travel-time distribution, or a calibrated confidence interval: scheduled and admitted realtime event times are point estimates.
Use reserves in a routing request
Control
Applies to
Meaning
minimumTransferBufferMinutes
Transit Route and Matrix, depart-at or arrive-by
Extra minutes before each subsequent boarding, after walking and published transfer minimums
arrivalBufferMinutes
Arrive-by transit Route and Matrix
Reserve the final minutes before the deadline and search for the latest departure that still arrives before the earlier planning deadline
Both are integers from 0 to 60 and default to zero. A positive arrival reserve needs a deadline at least as large as the reserve and a horizon exceeding it by at least one minute; the earliest departure stays the one implied by the original deadline and horizon. Transit via points reject a positive reserve because composing segments would apply it repeatedly, and Walk, Drive, Reach, and native operations reject a positive arrival reserve.
For Harvard Square to South Station, save reserved-arrival.json:
VIGO plans arrival by 08:55, allows departure no earlier than 07:00, and requires three extra minutes at each change. These numbers are illustrative preferences, not an MBTA delay estimate or a recommended threshold, and a tighter request can legitimately find no journey.
Route clocks and leg durations still describe the chosen timetable journey, and a Matrix scalar duration keeps its deadline-minus-departure definition, including the reserve. Keep diagnostics.timeReserves with the answer: it records the requested and planning deadlines, both margins, and calibratedProbability: false. Request diagnostics: "trace" to see it. A transfer reserve can absorb some lateness, but it does not model early departures, missed-connection recovery, capacity denial, cancellations, correlated delays, or uncertain walking and station access, and the final reserve alone does not protect intermediate transfers.
Calibrate probabilities before offering them
GTFS-realtime StopTimeEvent.uncertainty has no defined statistical meaning, and an omitted value means unknown. It must not become a standard deviation or a 90% interval without a validated, provider-specific model. See the GTFS realtime reference.
For a Boston calibration study, start with the MBTA LAMP exports and their data dictionary and TransitMatters Gobble. These are potential inputs; VIGO does not download or fit them. A credible next stage would retain historical forecasts and outcomes, match the exact feed revision and trip instance, estimate errors by route, direction, time of day, and lead time, and validate on later dates with coverage reporting. Same-vehicle and shared-corridor delays are correlated, so multiplying marginal transfer probabilities is not a journey probability. Probability-driven routing would need its own search and calibration contract.
Reproduce the implemented contract
Run node test/check-arrive-by-boundary.mjs after building the native kernel and npm run check:standalone after building the compiler. The public fixtures cover a changed selected departure, the exact reserve boundary, blocked outcomes, unchanged earliest departure, transfer-reserve combinations, matrices, invalid values, and repeated requests through both runtimes. They establish modeled feasibility, not measured on-time performance.
Route shows boarding prices from the imported GTFS feed. In Studio, expand the fare line for ticket names, payment methods, and the agency's fare link; JSON Results carry the same data in each transit leg's fare.
These are prices for a new boarding, not an optimized journey fare. Multi-leg results deliberately have no total, because joining legs, transfer discounts, passes, and existing tickets can change what a rider pays. A missing quote does not mean free travel, and path selection does not consider price.
Data and matching
The importer keeps fare tables and their route, station, agency, and calendar references in an optional fare_catalogs SQLite table. Catalogs travel with a City and stay separate when feeds are merged. Cities built before fares were added need their original GTFS reimported to keep fare data.
Fares v2 takes precedence when fare_products.txt is present. Matching uses route networks, station areas, rule priorities, fare timeframes, and the default rider category; product names and media come from the feed. MBTA's transfer_only extension is honored, so a free-transfer product never becomes a free new boarding.
Fare timeframes use the civil date and time at the validation stop, including its timezone, daylight-saving changes, times after midnight, and calendar exceptions. For a realtime-adjusted leg, the original scheduled stop sequence must be uniquely recoverable before timed rules apply.
Explicit platform fare areas override inherited station areas. Ambiguous network assignments, conflicting products, missing references, and unsupported conditions stay unquoted, and an unreadable fare catalog never discards a computed route.
Transfer totals, joined fare legs, pass selection, non-default rider eligibility, and fare-based optimization are not implemented.
Matching follows the GTFS Schedule reference. No agency prices or route-name rules are embedded in code; prices reflect the saved feed.
Performance
Fare lookup happens only after route selection, so no fare code runs inside routing, matrix, or Reach searches. Catalogs load by feed scope and are indexed once; quote, candidate, catalog, and scheduled-trip caches have explicit bounds and are released with their catalog. With cached catalogs, annotation performs no SQL. First-use parsing, serialization, and rendering still take time; measure them separately with node scripts/benchmark-fares.mjs FEED.zip STORE.sqlite YYYY-MM-DD report.json.
Verification
node test/check-gtfs-fares.mjs covers exact matching, missing data, published zero fares, invalid prices, transfer-only products, default rider categories, area inheritance, empty-rule semantics, priority rules, after-midnight date changes, time-window boundaries, scoped multi-feed import and merge, and non-mutating itinerary annotation.
Public v1 schemas preserve required fields, their types, units, and meanings. Removing or renaming a field, changing its units, or changing a status meaning requires a new schema version. Additional optional fields may be introduced; clients should ignore unknown fields. trace, native kernel structures, and Studio's internal worker messages are outside this stability policy. Result schema versions are independent of prepared City formats.
The 0.5.0 public schemas replace the older result envelopes; there is no legacy-envelope adapter. See upgrading for the City rebuild and rollback procedure.
Reading a result
The CLI, resident stream, and HTTP services use one public result contract. Read status first: ok contains the result, not_found means no journey satisfies this request, and error means the request failed. A valid no-journey response exits 0 and uses HTTP 200.
Route returns schema: "vigo.route.v1", query, journey, and meta. Matrix and Reach use vigo.matrix.v1 and vigo.reach.v1. The schema version and meta.engineVersion are separate identities.
Interactive Route commands show a short itinerary. Pipes, --output, and --format json produce structured JSON; --format text explicitly selects the terminal view. Diagnostic details are available in JSON.
Journey
journey contains departureTime, arrivalTime, durationSeconds, walkingSeconds, waitingSeconds, ridingSeconds, boardings, transfers, and legs. Drive journeys also have drivingSeconds. Clocks are local service-day HH:MM:SS, including hours above 23. Durations are integer seconds, rounded at the public boundary; routing retains its original precision. Waiting includes the gaps before and between legs. It is not inferred to be a reliability margin.
Legs have type: "walk" | "transit" | "drive", from, to, clocks, duration, and available distance. Transit legs identify the route and trip. Stop, route, and trip references use { "feed": "mbta", "id": "70067" }; an unscoped feed is null. You can send an endpoint as { "stop": { "feed": "mbta", "id": "70067" } }. Older string stopId requests remain accepted.
Route includes available GeoJSON geometry on each leg by default, preserving the full source coordinate precision and vertex sequence. A transit line runs from the boarding stop through the aligned shape vertices to the alighting stop, and its distanceMeters is the length of that line. A walk that starts or ends off the street network reports the short connection as endpointConnector. Send includeGeometry: false or use --include-geometry=false for a smaller response without coordinate arrays. Matrix journey geometry remains opt-in with includeGeometry: true. Geometry describes the modeled path, not an observed vehicle trajectory or a guarantee of surveyed station interiors. Ordered journeys have a single ordered leg list.
Additional journeys appear in alternatives, which excludes the primary journey and duplicates and is omitted when none remain. alternativeSearch describes how they were found. Requested alternative journeys (alternatives, alternativeRanking) are available in both runtimes and report coverage: "requested_time". Standalone journey windows sample later departures or earlier arrival deadlines and report their sampling direction.
Transit legs carry a fare when the City publishes GTFS Fares v1 or v2 data for the ride. Both runtimes price legs with the same engine.
Keep uncertainty with the answer
quality.streetGeometry distinguishes verified street evidence from unverified geometry. quality.stationPath is source_path or inferred when station access is present. A verified street segment does not certify a complete entrance-to-platform path. components separates street and station access costs and identifies their source types. Transit quality.schedule: "timetable" means modeled timetable times, not observed punctuality or a guarantee that every source timestamp was measured.
Route warnings describe qualifications that affect the returned journey. Dataset/model limitations live in vigo info (Rust), vigo inspect (Node), or GET /v1/info; send includeLimitations: true when needed alongside a query. No warnings does not certify complete source coverage. Unsupported GTFS semantics remain documented in known limits.
Diagnostics
Level
Added output
none (default)
Journey or analysis result only
summary
Available integer search counters and candidate counts
profile
Summary plus measured timings in integer microseconds
trace
Profile plus the full original internal result under trace
Use --diagnostics summary, a JSON diagnostics field, or ?diagnostics=summary. HTTP also accepts ?vigo_diagnostics=summary. Conflicting body and URL settings are rejected. trace is an unstable research/debug ABI: internal indices, sentinels, duplicate units, and raw candidate arrays intentionally remain there. Production clients should consume the public fields.
scannedDepartures is the engine's departure scan count; expandedTripRuns and relaxedStops are its reported expansion/relaxation counts. They are not interchangeable with network size or unique visited stops. Candidate counts count the returned endpoint candidate entries. Fields without instrumentation are omitted rather than fabricated. The Rust and Node runtimes return the same public result for the same request and City; summary and profile fields describe each runtime's own instrumentation and are not comparable across runtimes.
meta.computeUs retains the runtime's measured compute boundary, identified by computeScope: "query_dispatch_including_materialization": service-date activation, search, and result materialization. It excludes public formatting, final JSON serialization, HTTP queueing, and network transit. Profile scopes can overlap and must not be summed. Server-Timing reports measured serialization and compute; the Rust supervisor also reports queue time. Client wall time remains a separate measurement.
meta.requestId identifies a result; caller-supplied id is echoed. meta.queryFingerprint hashes the caller's request, with stop references canonicalized, and the City revision; output switches (id, diagnostics, includeGeometry, includeLimitations, reachFormat) are excluded. The same request has the same fingerprint in both runtimes. Equivalent requests written differently, for example time and timeMinutes, can hash differently. Supplied scenario and realtime content get separate fingerprints; retain the original inputs for reproducibility. Realtime admission/application evidence is retained when the runtime supplies it.
Matrix and Reach
Matrix returns durationsSeconds[origin][destination], with ordered endpoints in query. Unreachable cells are null, never zero. Optional journeys follows the same ordering. Arrive-by duration is the requested deadline minus latest departure; a nested journey can arrive earlier and have a shorter elapsed duration.
Reach returns surface.valuesSeconds, grid bounds and dimensions, cutoffsSeconds, GeoJSON contours/areas, and fullSurface when available. Cell order is unchanged: row-major, northwest first. Unreachable cells remain null. GeoJSON cutoff properties and explicitly requested street/node evidence retain their documented unit-labelled fields; these analytical evidence formats are separate from journey durations. Raw search chains are only in trace.
For a map, request reachFormat: "map". The result contains areas and bounds for the full reached extent, without raster values, stop lists or duplicate contours. Its polygons equal fullAreas in the default reachFormat: "full" response. Use the full response for grid comparisons and request includeNodes or includeEdges only with that format.
Compare saved results
Public saved results can be compared without rerunning routing. Changes are after minus before, in seconds. Route reports duration and transfer changes; Matrix and Reach report common, faster, slower, unchanged, newly reachable and no-longer-reachable counts. Means use only mutually reachable entries and are null when none exist. Matrix requires identical ordered endpoints; Reach requires the same grid. Callers must align other experimental assumptions.
For a source-data audit, request diagnostics: "trace" and retain the original witness under trace. Low-level native operations have a separate field reference.
Keep a reproducible run
Errors use schema: "vigo.error.v1", status: "error", and error.code / error.message. Handle stable codes and HTTP status, not OS error text. Keep the City, original query, executable version, supplied observations/scenario, and result together when retaining a reproducible run.
Untimed GTFS stairs and gates using routing estimates expose quality.stationTime: "estimated" on the walk. Their diagnostic provenance is gtfs_pathway_estimated; these costs are not published or measured traversal times.
Detailed wrapper transport
stream --stream-output=detailed sends a vigo.stream.detail.v1 envelope containing id, status: "ok", and trace, without constructing a second public result. The trace is the same complete internal result provided by --diagnostics=trace; it remains an unstable internal ABI. Envelope ok means execution completed: inspect the detailed result's routing status to distinguish a journey from a valid blocked outcome. Failures retain the standard vigo.error.v1 envelope and do not stop subsequent requests.
Clients must check capabilities.output.streamFormats for detailed before selecting this transport. VIGO-py negotiates it automatically and falls back to the full public trace response on earlier compatible runtimes. The default stream output remains the public result schema.
VIGO 0.5.0 enforces stop-transfer walking floors, OSM node restrictions, and explicit missing station coordinates. Both Node Engine and standalone Rust use the same prepared City data. Rebuild pre-0.5.0 Cities from the original GTFS and OSM inputs as described below.
Time and connectivity are separate
A GTFS transfer minimum constrains the time between services. It does not describe a physical path or permit arbitrarily fast walking. Before searching, VIGO prices each expanded stop pair at the larger of the published minimum and its distance divided by the configured walking speed, rounded up to a whole second. The distance is at least the straight-line separation of valid stop coordinates. That separation supplies a lower bound; a missing interior path remains unknown.
GTFS pathways describe directed station connections. An explicit traversal_time remains a source cost, including for lifts and moving walkways. If it is absent, VIGO derives a time from the available length and walking policy. A declared pathway supports the feed's connectivity model; it is not a current field measurement. When a station declares pathways and public entrances, coordinate access uses those entrances; snapping straight to a platform cannot bypass the station connections.
The walking floor enters preparation before boarding selection, so a depart-at query may choose a later connection and an arrive-by query may require an earlier departure. Route and both scalar and journey Matrix modes use those same costs.
Pedestrian permissions
The OSM importer reads tags on ways and their nodes, including packed dense nodes. It excludes foot=no, foot=private, and foot=use_sidepath, honors explicit pedestrian permission over generic access, and preserves directed pedestrian rules. A motor-vehicle one-way restriction alone does not prohibit walking in the reverse direction.
For nodes, impassable barriers and denied access exclude incident pedestrian segments. Ambiguous gates require explicit permission. Foot/access conditional rules and opening hours other than 24/7 are excluded because this static model does not evaluate their conditions. This conservative policy can reduce coverage, including approaches to a blocked barrier. Driving retains its separate permissions. See the OSM definitions of foot access and pedestrian direction.
What a returned route establishes
streetPathVerified concerns the prepared street graph; it is not independent proof of an entire door-to-platform walk. Free-coordinate snapping is returned separately as endpointConnector, with streetPathVerified: false. The walk's line still starts or ends at the requested coordinate, so a journey is drawn without gaps; its first or last segment is that connector. An assumed station interior has stationAccessStatus: "unverified"; a published transfer minimum does not change that status. Selected station costs and stop chains are exposed in accessCost when available.
An independent physical audit needs a continuous, directed source path, legal node/way access, and evidence for the links between the requested location, street, stop, entrance, and platform. A line near an OSM street is insufficient. Source gaps require better mapped entrances, agency pathways, or validated access links. Even complete source evidence does not establish current closures or observed travel times.
An interior GTFS node may omit coordinates. VIGO retains its declared pathway connections and stop ID but omits that node from displayed geometry, setting stationGeometryStatus: "incomplete". A line between the remaining known coordinates is schematic. If a source-timed pathway also lacks length and located endpoints, its distance contributes only a zero lower bound and the leg reports stationDistanceStatus: "lower_bound"; it is not measured zero-distance walking. When traversal_time is absent, stairs with a nonzero stair_count use one second per step and fare/exit gates use five seconds. These fixed routing assumptions are recorded as gtfs_pathway_estimated, retain stationAccessStatus: "unverified", and are floored by available distance at the configured walking pace. They are not measured or agency-published traversal times. Explicit traversal times take precedence. Other pathways without time, length, or distinct located endpoints remain excluded and reported as unpriced_pathways. The declared station topology remains in force, so exclusion cannot create a fallback shortcut. A feed-supplied connection alone does not certify its physical duration.
Upgrade and check
Build a new City with the current runtime and the retained raw GTFS/OSM inputs. Use the Boston quickstart and a new output directory, then open it with the engine as described in the engine manual. Keep the old City with its original runtime for reproducibility.
Street schema v4 cannot be upgraded by reopening a cache: its missing node restrictions require reimport from the original PBF. The current street schema is v6; it also excludes platform area outlines from walking links. Routing-store schema v3 distinguishes missing coordinates and pathway costs from genuine zeros and requires a fresh GTFS import. Transfer semantics v4 additionally requires reimport to recover pathway modes and stair counts discarded by earlier imports. Prepared access policy v9 connects street transfers to public entrances and composes their directed station pathways during preparation and prices travel between projections on the same street segment directly. Each boarding transfer can include at most one external transfer edge; the itinerary retains every original pathway segment. Declared pathways suppress invented parent-station shortcuts. Node Engine refreshes outdated timing preparations; standalone Rust requires current prepared data and rejects stale street, routing-store, or transfer-time policies.
Public regression checks use generated inputs and can be run from the source checkout:
Shell
npm run check:osm-pbf
npm run check:national-focused
npm run check:standalone
These checks exercise node barriers, access overrides, malformed node tags, pedestrian direction, physically feasible transfer selection in both time directions, scalar/journey matrices, and rejection of old prepared data. They test the implementation and declared input model, not the completeness of any agency's pedestrian map.
Open a JSON response from the VIGO engine, compiler, or HTTP API. The viewer shows its status and main answer, draws a journey timeline, a matrix table, or a Reach raster, and keeps the complete source JSON alongside.
Read the public fields
Route uses journey, Matrix uses durationsSeconds, and Reach uses surface.valuesSeconds or map-only areas. Durations are seconds and a null analytical value is unreachable. Keep geometry qualifications and warnings with the answer.
Choose or drop a file in the interactive viewer. Files are read on your device and never uploaded; the viewer accepts up to 25 MiB.
Python exports
Python's Result.to_dict() keeps its detailed, minute-valued payload. Use the Python Result reference to interpret those exports; this viewer accepts public engine schemas only.
Open a Result
Files are read in this window and never uploaded. Up to 25 MiB.
VIGO models the City revision and query it is given. It does not certify real-world service, demand, safety, or operational feasibility.
GTFS
Support is limited to the source features listed in GTFS support. Unsupported or malformed features are reported during the build.
Missing published shapes do not prevent timetable routing but limit map geometry.
Block-based in-seat continuation is not inferred.
Transit Route and Matrix
Arrive-by Matrix durations include any wait between actual arrival and the deadline; use Route or Matrix journeys for actual arrival.
Transfer and arrival reserves can change route selection, but VIGO does not compute calibrated arrival probabilities or delay distributions.
A finite maxTransfers cap with ordered transit waypoints is unsupported.
Station returns may be valid in the supplied timetable; they are flagged, not excluded.
Without an explicit rule or declared pathway topology, platforms sharing a parent station use a transfer assumption of at least 120 seconds, floored by their coordinate distance at walking speed. These legs report transferSource: 'parent_station_fallback' and schematic geometry, not a verified station pathway.
A street route to a platform coordinate does not establish an entrance-to-platform connection. Walks touching subway stops or platforms report stationAccessStatus: 'unverified' unless they carry a declared pathway with no estimated segments; streetSegmentVerified keeps the narrower OSM result. Untimed stairs and gates use explicit routing estimates (one second per step, five seconds per gate), marked gtfs_pathway_estimated. Scalar Matrix results do not carry this per-leg audit.
Departure windows sample integral minutes; they are not continuous profiles.
Streets
Walk and Drive depend on OSM coverage and directionality.
Pedestrian attachment searches vertices within 160 m and projects onto reciprocal edges within 80 m. A missing attachment is not proof that no physical path exists.
Platform polygons describe an area, not a walking line. Street-store schema v6 excludes their outlines from the walking graph; routing across polygon interiors is not modeled, and older Cities need a source rebuild.
An anonymous endpoint on an unanchored fragment can recover to the nearest transit-connected component within 80 m, including the full connector cost. This adds no graph links and proves nothing about buildings, fences, water, or levels in between. See coordinate attachment.
Authorized endpoint access is an opt-in build model; it does not establish individual permissions, gate hours, or missing connections.
Drive does not yet model every turn restriction, signal, or intersection delay. Traffic must be supplied by the caller.
Realtime
Realtime transit applies to Route only, and only with a supplied realtimeSnapshot and explicit realtime selection. Transit Matrix and Reach reject realtime requests. Vehicle Positions and Alerts do not change route costs or close services.
Only FULL_DATASET feeds are decoded. Updates modify matched scheduled trips, remove CANCELED and DELETED trips, and omit SKIPPED calls. Added, duplicated, replacement, and unscheduled trips without a supported scheduled instance are unsupported for routing, though Studio can display them.
Feed and record timestamps must pass freshness checks: up to 180 seconds old and 60 seconds ahead of the captured clock. Missing timestamps do not establish freshness. See realtime routing.
City reuse and platforms
Copy the entire City directory; street indexes and prepared files are part of it. Executables are specific to an OS and CPU, while City data is portable across the supported 64-bit targets.
Older Cities with ephemeral Drive CCH rebuild that hierarchy on fresh-process startup; rebuilding the City from source enables persisted Drive CCH.
An incompatible timetable cache, a new active-service pattern, a changed walking policy, or an evicted snapshot requires preparation. Missing or corrupt required street indexes are errors; restore the complete City or rebuild it.
Studio project settings, drafts, and live connections are local application state and do not travel inside a City. Studio cannot open compiler City directories in this release.
Scenario and Reach
Planned transit service changes apply to Reach; supplied traffic applies to Drive Route and Drive Matrix. Other combinations are rejected.
maxTransfers limits changes between vehicles in both baseline and scenario Reach; zero permits one ride, and walking never counts as a boarding.
After the last ride, Reach follows the same declared station exits as coordinate egress, and a stop reached only by a transfer walk does not restart the walking allowance. Reach rasterizes reached street edges while Matrix evaluates destination coordinates, so their cells can differ under matching settings.
Reach measures modeled travel time, not people, jobs, demand, welfare, observed behavior, or operational feasibility.
VIGO Studio puts Network, Route, Analyze, and City views over the same routing engine. It keeps its own project library and works offline once a City is imported.
Documentation and settings
Open Settings → Documentation from the City library or the sidebar to read this guide offline, in Studio's light or dark appearance. Press / to search; links to source files and external references open in your browser. Reading documentation does not open a City or prepare routing data, and the API schema download is bundled locally.
Open a City
Select a project from the Studio library and import GTFS and OSM. Rebuild when the source feed or street extract changes. Studio stores projects in its library format and cannot open the movable City directories built by the compiler in this release. Opening a City prepares walking for transit access and walking routes; the driving network opens when you select Drive. Background tasks shows preparation progress.
Network
Use Network for reporting coverage and briefings, and Routes for trip times, station boards, and line views. Route and station selection is shared across the tabs. Network and Routes explains playback, live timing, added service, and model connections.
Route
Plan
Pick an origin and destination on the map. Add point inserts a via point before the destination, up to eight points in total; click a row to repick it, use the arrows to reorder it, or remove it, and Reverse flips the sequence. You can also type a coordinate command in Search VIGO, such as route 38.90, -77.05 to 38.91, -77.03 at 08:00, then choose Plan this journey. The route form does not search place or station names.
Choose a travel mode. Realtime transit departs now; Scheduled exposes the date, time, and depart-at or arrive-by controls. Transfer caps are disabled while via points are present. Changing points or options updates the route, Rerun route submits the same coordinates again, and New route clears them.
Sort journeys by Earliest, Fewer transfers, or Less walking. Each card shows arrival, total time from your requested departure, transfers, walking, and its trade-off against the earliest arrival. Sorting keeps the selected journey and does not rerun the search; arrow keys, Home, and End select another journey, and Details opens its itinerary.
Save a journey
Choose a format beside Save in the journey details. JSON keeps the selected plan, diagnostics, and a summary separating the initial wait, journey duration, and total elapsed time. Markdown is a readable itinerary whose clocks can exceed 23 hours after service-day midnight. GeoJSON keeps leg geometry and its source when the complete path is retained. CSV records each leg's trip and service-clock times; clock_service_date names the midnight those minutes count from, and minutes can exceed 1440. Saving never reruns the query.
Save all beneath the journey choices keeps every completed alternative, including those beyond the five visible cards. Filenames include the service date, places, and export time.
Recent
Reopen recent Route Results for the current City revision. Each keeps its normalized request, status, warnings, and timing beside the journey.
Analyze
Reach
Choose an origin on the map, or use Choose origin to search imported stops by name or ID or enter coordinates. Run Reach, then switch between Reachable area, Reached streets, and cutoffs; changing the displayed cutoff reuses the retained result. See Reach for limits and bounds.
Compare
Studio's feed comparison runs Reach for each selected GTFS feed and shows the surfaces. City comparisons mark the first selected feed as the reference and show sampled-area and reached-stop differences at the selected cutoff. Scenario cases show the same measures against their baseline, together with faster or newly reached and slower or lost cell counts. Differences require the same origin, date and time, mode, sampling, and walking and transfer settings; cell counts are never subtracted across independent grids. To compare completed Results without rerunning them, use the compiler's compare.
Use Save beside Reach or comparison results for the full JSON, the selected cutoff's area geometry, or a CSV summary. total_processing_ms measures complete analysis processing, separate from native query timings; area_source distinguishes an exact-cutoff area metric from a display-raster calculation; reference_surface and reference_feed_id identify the baseline. Saving becomes available when the computation finishes.
Scenario drafts and the selected case are saved in Studio's local profile and restored when you reopen the same project and source revision. They do not travel with a City directory; reimporting GTFS or rebuilding the project starts a separate draft set. For a stop inserted on an A → B edge shared by several branches, the road path applies to each affected branch, and each branch keeps its own untouched shape and runtime. Use the engine, the compiler, or Python for Matrix queries.
City
Data sources
Open City → Data sources to review GTFS feeds, OSM coverage, counts, and warnings. To remove a source, select the trash button beside its row, check the name, and confirm Delete source. Removing a GTFS feed refreshes the combined timetable from the remaining feeds; removing OSM removes street routing and OSM-derived walking transfers until another import. The City, other sources, saved notebooks, and your original files are kept, and deletion is unavailable during import or preparation.
Preferences
Choose appearance, storage location, and map preferences. The default City folder is ~/Documents/Vigo Projects. On macOS, Studio does not scan or test-write the library until you choose Open library folder, when macOS may ask for permission. Locally rebuilt apps use ad-hoc signatures, so an updated build can need authorization again.
Local OSM is the default basemap. It draws main roads, rivers, lakes, and coastal water from the City's imported PBF in light or dark appearance, with no tile service, API key, or network connection; residential streets, paths, buildings, and labels are omitted to keep routes and analysis clear. Cities imported before the local map index need one fresh OSM import. Online OpenStreetMap and CARTO styles remain optional.
Studio troubleshooting
Playback shows no trips
Check the exact service date, calendar.txt, calendar_dates.txt, the selected direction and pattern, and whether the playback time falls between a trip's first departure and last arrival. A route-wide total is not the number of vehicles active at once.
An access path crosses a block
The access or egress line must come from OSM street geometry. Rebuild from current OSM data, confirm the point lies within coverage, and inspect the selected street leg. Studio never replaces a missing street path with a straight jump.
A date is rejected
Use an exact YYYY-MM-DD date inside the feed's active service range. Studio applies the same calendar rules as the command line.
A loading indicator is not advancing
Background tasks show measured bytes or records for steps with a known total; the percentage belongs to that step, not the whole import. Work without a total shows an indeterminate bar, elapsed time, and the age of the last update. If updates pause, use Reconnect to task: the task may still be running, so check its status before importing the same source again. Confirm that the City folder is readable and writable and that macOS has granted access.
Open Network in VIGO Studio to inspect the selected City's timetable, live reports, and retained briefings. The workspace has Network and Routes tabs; use Route for journey planning and Analyze for Reach.
Read the evidence
The same map can show several kinds of evidence. Read the source and observation time before interpreting a change.
What you see
What it represents
How to use it
Scheduled time
An indexed GTFS arrival or departure on the selected service date
Establish the published plan; check the date and pattern
Predicted time
A matched, admitted TripUpdate for that trip and stop
Compare the same arrival or departure with its schedule
Vehicle position
A reported location and stop status at its own observation time
Locate the vehicle; check its age separately from the prediction
Estimated playback
A position interpolated along the scheduled trip
Explore the timetable and geometry
Network assessment
Computed conditions among reporting service in a stated window
Read coverage alongside delay and spacing
Model explanation
An interpretation of the supplied evidence
Inspect sources, uncertainty, and the proposed next check
None of these is a verified history of actual stop arrivals, and a missing prediction stays unknown. A vehicle marker or an added trip in Trip times does not mean Route can use that trip; routing has its own admission rules.
Investigate a condition
Start with coverage. Check the assessment time, service window, and reporting coverage in Network. Coverage weights scheduled vehicle-minutes with usable predictions or cancellation reports; it is not the share of passengers covered or service running on time.
Follow the route. Open Routes, select the route, and inspect Trip times on the relevant date. Use Stops to confirm direction and pattern, then Updates for service evidence.
Inspect the same event. Open the station board or select a vehicle in Line view. Compare arrival with arrival, or departure with departure, at the same stop, keeping both observation times visible.
Keep the observation attached.Export observations or Export report retains the current evidence, and Export evidence in the service briefing saves its assessment and source checks.
Network shows reporting coverage, route comparisons, and the network briefing. A scheduled departure, a feed prediction, and an observed vehicle location are different evidence, and missing reports remain unknown. Overnight service is assessed against the timetable's active window.
With a connected model, the briefing can interpret a pattern, propose an explanation, and suggest a next check. Hypotheses are not confirmed incidents. The briefing checks extracted route-condition claims against computed evidence and performs a model review, but neither guarantees that all prose is correct; a computed fallback is labeled separately.
Inspect a route or station
Open Routes and choose a route to see Trip times; select a trip and date to compare scheduled and predicted arrivals or departures. Stops opens directions and patterns, and Updates opens service evidence. The map and Line view show the same route. Select a stop for its arrivals board, and All routes returns to the catalog. Route and station selection is shared across Network, Routes, the map, and the line view; Network map clears both.
Patterns and scheduled playback
A route's Stops view shows directions, patterns, ordered stops, service span, and published geometry. Patterns are labeled by first and last stops, and Full service restores them all. Direction IDs 0 and 1 are feed labels, not compass directions. Technical IDs appear under Source data. Times such as 25:10 stay on their GTFS service day.
Scheduled playback is labeled Estimated: vehicles dwell at stops and follow their pattern's shape between timed calls. The index does not keep shape_dist_traveled or every untimed call, so interpolated positions are not exact. Live positions stay separate from playback, and route-colored vehicle circles show a bearing arrow when available.
Vehicle timing and line view
In Network → Routes, select a route and choose Line view. The two tracks show each direction's stops and reported vehicles. Select a vehicle on the map or the line to compare scheduled and predicted arrival and departure at its reported stop. If that stop has no usable timing, its prediction stays unavailable; Next prediction names a later stop with one without substituting its time.
Vehicle timing comes from the City's connection store and the current realtime snapshot. The browser requests only the selected route or vehicle every ten seconds, without overlapping requests.
Stop placement follows the VehiclePosition stop ID, sequence, and status; it never uses road snapping or interpolated movement. Missing or conflicting identities stay unplaced.
Arrival comes from the incoming connection and departure from the outgoing one, and delay compares the same event at the same stop.
Opposite directions share a central station list only when their station sequences are exact reverses. Bus stops on opposite sides of a street are not paired by proximity or name.
Branches and repeated stops stay distinct. Skipped stops, missing predictions, duplicates, and stale observations never become valid current timing.
Spacing is schematic: it shows stop order, not distance, time, or headway. The ↔ warning marks both vehicles in a compressed departure pair, comparing their predictions at the same stop against their scheduled interval.
Added service
Trips declared by GTFS-RT ADDED or NEW appear in the line view and the Trip times selector even when absent from the static timetable. Their reported stop sequences supply placement and timing; partial sequences are labeled as reported stops, and scheduled times and delay comparisons are unavailable. Added-service display does not make a trip available to journey routing; see realtime limits.
Verification and references
test/check-agency-route-line.mjs and test/check-added-service-display.mjs run with npm run check:agency. They cover both directions, branches, repeated and terminal stops, conflicting sequences, stale and future positions, skipped and no-data predictions, and added trips; they verify fixture behavior, not field accuracy. The interaction design drew on TransitMatters Train Tracker and Swiftly's Live Operations overview; no implementation or assets were copied. Matching follows the GTFS Realtime reference.
Connect a model
Open Network → Service briefing → Connect AI, choose a provider, enter its API base URL, choose or discover a model, and connect. The connection test checks function calling, not answer quality. Local models can use a keyless endpoint if their server allows it, and API keys stay in server memory for the session. Briefing requests include the computed assessment and selected source checks; the briefing does not search the web. A localhost URL does not prove where inference runs or whether data is retained.
What this app does not establish
Predicted spacing is not measured headway, and retained forecast changes are not actual vehicle progression. Reporting coverage is not service health. The briefing has no crew roster, maintenance clearance, demand model, or intervention simulator, and rider drafts require staff review; VIGO does not dispatch or publish messages. The operations ledger and synthetic replay are research interfaces.
VIGO 0.5.2 · CLI and HTTP reference · Prepared City format 1
VIGO runs routing and isochrone queries from a single Rust executable. The executable contains the routing kernels, City loader, JSON interface, and HTTP server. It needs no Node, Python, browser, external routing service, or internet connection at query time. SQLite is compiled in. City data is supplied separately and opened read-only.
This manual describes the standalone Rust interface. Requests retain vigo.standalone.query.v1; public results share vigo.route.v1, vigo.matrix.v1, and vigo.reach.v1 with the Node CLI. Studio and the private Python worker protocol remain separate. Run vigo capabilities to identify the executable before integrating it.
This manual is also part of the searchable, offline VIGO guide, which the engine serves at /. The OpenAPI specification contains the HTTP request contracts and native input/output types. The audit record describes tested coverage and remaining boundaries.
Use the command line to inspect a City, run a query, or start a service.
Commands
All paths are local filesystem paths. Every data command requires --city DIR or VIGO_CITY; this includes compare. Help, version, and capabilities need no City.
Command
Purpose
Main input
help, --help, -h
Print the command overview
None
version, --version, -V
Print version and runtime
None
capabilities
Supported operations and compatibility flags
None
info
Loaded City identity and summaries
City
route
One origin–destination route, optionally via/window
JSON or endpoint flags
matrix
All origin–destination travel times
JSON arrays
reach, isochrone
Reachable streets, raster, polygons, isolines
JSON or origin flags
compare
Changes between two saved Reach results
JSON with before and after
native
Low-level Rust operations
operation and typed input
stream
Resident NDJSON request/response process
One JSON object per stdin line
serve
Resident HTTP service
City and server flags
health
Check a running service's readiness without loading a City
--host, --port
Use vigo route --help for the general command overview. --name value and --name=value both work. Unknown options, irrelevant options, repeated options, and extra positional arguments fail. Boolean flags are bare for true or use --pretty=false; --pretty false is not the boolean syntax. The internal _worker command is reserved for the HTTP supervisor.
Query flags
Flag
Applies to
Meaning
--city DIR
Data commands
Prepared City; overrides VIGO_CITY
--request FILE
Route, Matrix, Reach, Compare, Native
JSON file; - reads stdin
--output FILE
One-shot JSON commands
Save result; - means stdout
--pretty
One-shot JSON commands
Indent JSON
--stream-output public|detailed
Stream
Public results by default; detailed internal wrapper envelope when explicitly selected
--service-date YYYY-MM-DD
Route, Matrix, Reach, Native, Stream
Set or override service date
--time HH:MM[:SS]
Route, Matrix, Reach, Stream
Set clock; replaces JSON timeMinutes
--mode transit|walk|drive
Route, Matrix, Reach, Stream
Reach supports transit/walk only
--from lon,lat or --from stop:ID
Route, Reach
Origin
--to lon,lat or --to stop:ID
Route
Destination
--max-walk KM
Route, Matrix, Reach, Stream
Walking endpoint budget
--max-transfers N
Route, Matrix, Reach, Stream
Transit transfer cap
--max-street KM
Route, Matrix
Independent street-route distance cap
--horizon MINUTES
Route, Matrix
Search horizon
--arrive-by
Route, Matrix
Arrive-by time direction; false restores depart-at
--alternatives N
Route
Additional transit journeys, 0–9
--alternative-ranking pareto|ranked
Route
How alternatives are chosen
--cutoffs 15,30,45
Reach
Travel-time cutoffs
--raster-size N
Reach
Square grid width and height
--radius KM
Reach
Reporting extent radius
--street-edges
Reach
Include directed street evidence
--scenario ID
Data commands on a collection
Select a scenario; omission uses defaultScenario
--wheelchair
Route, Matrix, Reach
Require the City's strict wheelchair profile
--format text|json
One-shot JSON commands
Terminal text or JSON; pipes and files use JSON
--diagnostics LEVEL
Query commands
none, summary, profile, or trace
--include-geometry, --include-limitations
Query commands
Route geometry is on by default; Matrix geometry and limitations are opt-in
Server flags are listed in the HTTP API reference. Other controls, including transfer buffers, realtime, scenarios, via points, and journey geometry, belong in JSON. CLI query flags override fields from the request file. Stream flags override every line, so omit a stream-level service date when sending different dates.
Files and standard input
Input files/stdin accept one JSON object and an optional UTF-8 BOM, up to 8 MiB. A request read from stdin completes when stdin closes. JSON goes to stdout; logs/errors go to stderr. With a file output, the result is saved instead of printed. Writes are staged and renamed atomically, preserving an existing file's permissions. The output directory must already exist. No CSV batch interface is provided; use Matrix or Stream.
The compiler, vigo-build, builds Cities and runs Route, Matrix, Reach, Stream, and Compare through VIGO's shared native routing kernel. The engine has its own command set; see the engine CLI reference.
Commands
Command
Input
Output
build
GTFS, OSM, output directory
A complete prepared City
build-scenarios
A scenario source manifest
A prepared scenario collection
capabilities
None
Versions and supported query combinations
inspect
City directory
Identity, sources, counts, and limitations
route
City and JSON request
A public journey Result, or a CSV batch
matrix
City and JSON request
An origin-by-destination travel-time Result
reach
City and JSON request
Reachable areas and optional analytical output
stream
City and NDJSON on stdin
One Result per request from a resident process
compare
Two saved Results
Changes without rerunning routing
Run vigo-build help route or vigo-build route -h for options, and vigo-build --version for the version. Unknown options, extra positional arguments, and repeated single-value options are rejected; --gtfs, --gtfs-scope, and --prepare-date may repeat.
Send a request
Requests carry serviceDate and time in JSON, and flags override request fields exactly as in the engine:
Request files and stdin share a 16 MiB limit, and --request - reads until the producer closes stdin. Progress and errors go to stderr; JSON goes to stdout. Saved output is staged and renamed after a complete write, so a failed run can leave an earlier output file in place: check the exit code. CSV Route batches use --input with an output file, support transit only, and cannot be combined with --request.
Keep the compiler resident
stream reads one request per line, each with an explicit kind, and keeps prepared network state between lines. Each line may choose its own serviceDate; a --service-date flag only sets the default.
Responses echo id and add sequence. A per-line error does not end the stream. First-use preparation, later queries, and serialization are different timing boundaries.
Read a response
Public Results identify schema, status, and query metadata: Route uses journey, Matrix durationsSeconds, and Reach surface.valuesSeconds or map-only areas, all in seconds. diagnostics: "trace" adds internal evidence under trace; applications should use the public result reference. An invalid one-shot request exits nonzero, and stream errors use schema: "vigo.error.v1" with error.code and error.message.
VIGO Python runs the same resident protocol with its own Result properties. Studio's internal application API is not a public integration boundary.
Queries accept map coordinates or stop IDs. Transit queries also need a service date and a clock time.
Coordinates and stop IDs
Use [longitude, latitude] in WGS84 decimal degrees. Longitude must be −180…180, latitude −90…90. Kilometers are used for high-level walking/street limits; output geometry distances are meters; high-level times are minutes. Native operations use seconds/meters unless a field explicitly names another unit.
JSON
{"coordinate": [-77.05, 38.9]}
JSON
{"stopId": "A"}
A point may also be a bare coordinate array. Point objects accept id, name, label, source, and scenario metadata editStatus, baselineStopId, baselineStopIndex. These labels do not change routing. A valid stopId takes precedence over a coordinate, except source: "map", which deliberately uses the coordinate. Selected stations/platforms include their prepared station members. This selection does not certify a physical path between every platform.
Exact identifiers are case-sensitive. Multi-feed IDs may contain a feed scope and the JSON separator \u001f. Obtain active IDs with timetable.identifiers; do not assume a local ID is unique in a combined City.
Common query fields
Field
Default / requirement
Accepted values and meaning
serviceDate
Required for transit/timetable operations
Exact YYYY-MM-DD; calendar plus date exceptions select service
serviceDay
Derived
Optional weekday, saturday, or sunday; must agree with the date
time
Required unless timeMinutes is supplied
HH:MM[:SS], hours 0–71; numeric minutes are also accepted
timeMinutes
Alternative clock
Number 0–4319; never combine with time
timePreference
depart_at
depart_at or arrive_by; aliases depart and arrive accepted
mode
transit
transit, walk, drive; Reach has no drive mode
maxWalkKm
1.2
Number 0–100; per transit access/egress endpoint or Reach terminal-walk budget
maxTransfers
No explicit cap
Integer 0–31; zero means at most one boarding
horizonMinutes
480
Number 1–2880; Route/Matrix only
maxStreetKm
50
Number 0.05–1000; Walk/Drive Route/Matrix limit
allowStreetTransfers
true
Keep or filter walking transfers between separate stops/stations
minimumTransferBufferMinutes
0
Integer 0–60; extra time per subsequent transit transfer
requireTransitRide
false
Transit Route/Matrix boarding policy
allowLongWalk
true
When false, cap direct walking by maxWalkKm instead of maxStreetKm
walkSpeedKph
City's walking policy, normally 4.8
Number 1–8; Walk Route/Matrix and Reach only
disableCache
false
Disable street endpoint caches; not a complete cold-run switch
requireCompleteServiceCoverage
false
Reject missing active source scopes in a multi-feed service slice
routingDataMode
Inferred from supplied observation input
scheduled or realtime; dataMode is an alias; do not supply both
id
Optional
Caller correlation value; echoed by Stream and HTTP
Service time and walking policy
Transit times are service-day clocks, including after-midnight GTFS hours. Calendar queries include services on adjacent dates when their trips overlap the search interval. GTFS clocks use local noon minus twelve elapsed hours, including daylight-saving changes. journey.clockDate anchors every journey and leg clock; a transit leg's serviceDate identifies its actual operating date, and hours can exceed 23. Street-only queries still require a clock but can omit serviceDate.
The depart-at timetable search ends at departure plus horizon; arrive-by searches back to the later of midnight and arrival minus horizon. A transit egress walk may extend beyond the depart-at scan horizon. Direct walking is bounded by physical walking duration as well as its distance cap. Drive routing uses its street-distance limit and supplied static metric; horizonMinutes does not add a drive travel-time cutoff.
City access padding/overhead apply to boarding access, not physical direct-walk duration. The transfer buffer applies once after walking and source-defined transfer minima; it does not delay first boarding, final egress, or staying aboard. allowStreetTransfers: false retains endpoint access and egress. Neither control establishes station accessibility or fare-transfer eligibility.
arrivalBufferMinutes is an optional integer 0–60 (default 0) for arrive-by Transit Route and Matrix. A positive value reserves time before the final deadline while retaining the original earliest departure. The deadline must be at least the reserve, and horizonMinutes must exceed it by at least one minute. Positive reserves reject via points, depart-at, Walk, Drive, Reach, and native operations. diagnostics.timeReserves records both deadlines and calibratedProbability: false. Route and journey clocks retain their actual modeled times; Matrix scalar durations include the reserve. Combine with minimumTransferBufferMinutes for time at intermediate changes. See Travel-time uncertainty for the Boston example and calibration requirements; a margin is not a probability guarantee.
Route finds a journey between an origin and a destination at a chosen time. Transit queries also require a service date. A valid query with no journey returns status: "not_found".
Reference fixture
The remaining reference examples use the small public test network A → X → B, with service on 2026-07-15. It is also used by the localhost demonstration on port 8787. These IDs do not belong to Boston; use the coordinates and data above for your own first queries.
A depart-at request requiring transit, so the fixture illustrates ride and transfer clocks:
The journey departs at 07:55 and arrives at 08:30, with 25 minutes riding and 10 minutes waiting. transfers: 1 means two boardings. Public journey durations are integer seconds, and journey clocks are local service-day HH:MM:SS. See Results and errors for the full response structure.
Change mode to drive and remove walkSpeedKph for a drive query. Street paths follow directed OSM routing, including snap diagnostics; drive geometry uses its routed node sequence.
Direct walking policy
For transit coordinate requests, a direct OSM walk competes with transit by default. Set requireTransitRide: true to require a vehicle boarding. A blocked request containing an explicitly selected stop does not gain a direct-walk fallback. A ready selected-stop route can still be replaced by a winning direct walk when that policy is false. Set mode: "walk" when walking is the intended operation.
Journey geometry
Route includes its materialized leg geometry by default. Set includeGeometry: false to omit coordinate arrays from the public response. Matrix geometry remains opt-in. A transit leg's line runs from the boarding stop through the aligned source-shape vertices to the alighting stop; distanceMeters is the length of that line. Missing source shapes fall back to a labeled stop sequence. Transit legs carry GTFS fare annotations when the City publishes fares. Studio's balanced/preference presentation is not part of this interface.
Ask for several journeys at one time, plan through intermediate points, or compare departures over a time window.
Alternative journeys
alternatives (0–9, default 0) asks a depart-at transit Route for additional transit journeys at the requested time. The primary journey stays in journey; the others appear in alternatives, earliest arrival first. Both runtimes return the same alternatives for the same request.
alternativeRanking chooses them:
pareto (default) keeps only trade-offs: an alternative must beat every other returned journey on arrival, boardings, or walking. It uses no more boardings than the primary journey. When more trade-offs exist than requested, the fewest boardings and the least walking are kept first.
ranked keeps the earliest arrivals even when another journey is better in every respect, then prefers fewer boardings and less walking. It admits one more boarding than the primary journey, within maxTransfers. Journeys on the same sequence of vehicles count once, whatever stops they use to change or leave.
alternativeSlackMinutes (0–120, default 15) bounds how much later than the primary journey an alternative may arrive. alternativeSearch records coverage: "requested_time", ranking, maximumJourneys (including the primary), arrivalSlackMinutes, and status. A search that exceeds the kernel's label budget reports status: "unavailable" with a reason and returns the primary journey alone. Alternatives are transit journeys; a faster direct walk remains the primary journey. Requests that combine alternatives with arrive-by, via points, or a journey window are rejected.
Supply via (alias waypoints) as an ordered array of at most 16 points. Do not supply both aliases. Each segment is routed in order, carrying time forward for depart-at or backward for arrive-by. Results contain segments, not one flattened journey. A blocked segment reports reason: "via_leg_blocked" and legIndex.
Transit via requests require a boarding on every segment. They reject an explicit maxTransfers or a nonzero transfer buffer because independent segment composition cannot certify the transfer boundary. No dwell/visit duration is added at a via point.
Journey windows
windowMinutes accepts 0–240 (default 0). A positive value samples searches at windowStepMinutes intervals (1–60, default 1), including the requested time. Depart-at searches move toward later departures; arrive-by searches move toward earlier arrival deadlines. Results retain up to five distinct, nondominated journeys, sorted by earliest arrival for depart-at or latest departure for arrive-by, then fewer boardings and less walking. The primary journey is in journey, with the remaining choices in alternatives.
alternativeSearch records minutes, stepMinutes, searches, and direction (later_departures or earlier_arrival_deadlines). Transit alternatives at each sample use a 15-minute slack; the direct-walking comparison can lie outside that envelope. This is a sampled search, not a continuous all-departures guarantee. A nonempty via list takes precedence and removes window sampling from its segments; do not combine the two when you need window alternatives.
These fields are supported by the standalone Rust CLI and its HTTP service. The Node CLI instead supports --departure-window or JSON departureWindowMinutes for a centered depart-at profile of ±0–30 minutes. It rejects the standalone window fields, and rejects positive centered profiles with arrive-by, rather than silently running a point query.
Matrix calculates travel times between every origin and destination in a request. It supports transit, walking, and driving.
Build a matrix
Supply origins and destinations arrays with at least one point each. Use point objects or coordinate pairs as entries. A request can contain up to 65,536 origin–destination pairs. Row order follows origins; column order follows destinations.
durationsSeconds[row][column] gives seconds from the shared departure to arrival for depart-at, or from the latest departure to the shared deadline for arrive-by. In the latter case, it can exceed the accompanying journey's duration when the journey arrives before the deadline. JSON null means unreachable. See Matrix output for indexing, examples, and journey details.
Include journeys
includeJourneys defaults to false and is supported for transit only. With true, journeys has the same dimensions and nulls for unreachable pairs. includeGeometry defaults to false and requires journeys. When direct walking wins, its journey contains a timed walking leg, with geometry if requested. Large journey/geometry matrices can reach the HTTP response limit; split them into smaller requests.
journeyFormat defaults to "full", retaining stop names, route details, stop sequences, and walking evidence. Analytical callers can explicitly request "compact" with includeJourneys: true: it returns the same selected trips, stops, boarding sequences, leg clocks, transfers, and walking/riding/waiting durations without display metadata or walking-evidence annotations. Compact does not accept includeGeometry: true. For duration-only workloads, leave includeJourneys false. Compare performance at the same output detail; the Node Matrix interface returns compact witnesses.
Repeated endpoints
Transit Matrix keeps repeated endpoints grouped through transit search, direct walking and journey geometry, then expands completed answers into the requested row and column order. Exact-stop and coordinate endpoints retain their respective routing semantics. Trace diagnostics expose matrixReuse.requestedPairs, searchPairs, uniqueOrigins and uniqueDestinations. The 65,536-pair response limit still applies; this reuse does not approximate or merge distinct endpoints.
Run node scripts/benchmark-service-reuse.mjs --reference /path/to/baseline-vigo --samples 31 --output comparison.json to compare two executables on a generated public fixture. The check compares complete answers and reports date preparation, retained-date switches, repeated and distinct Matrix pairs. Engine execution time and the detailed-output round trip are recorded separately. This small fixture tests reuse and regressions; metropolitan scaling needs a separate workload.
Reach finds the streets and areas accessible from an origin within a set of travel-time limits. Transit queries combine timetable travel with walking over the prepared street network. Use mode: "walk" for walking only, or call isochrone as an alias of reach.
Reach supports depart-at queries. Drive and realtime transit are unavailable in Reach; see Compatibility for the supported combinations.
Number 1–40; reporting extent, not a trip-distance cap
bounds
Derived from origin/radius
[west,south,east,north], finite and ordered geographic limits
includeStreetEdges
false
Include indexed directed street evidence
includeNodes
false
Include native node evidence
surfaceSampling
street
street samples reached street intervals; cell-center routes to every grid center
scenario
None
Exclusions, planned services, or compiled overlay
Do not supply horizonMinutes, maxStreetKm, requireTransitRide, or allowLongWalk to Reach. The largest cutoff bounds the analysis. Walk-only Reach accepts a clock without a service date and cannot apply a transit scenario.
Cell-center sampling uses the Matrix walking allowance and fixed requested bounds. It supports walking or scheduled transit, without planned service changes. Transit uses the City's prepared 4.8 km/h walking policy; other speeds are rejected. Street sampling supports planned service changes and adjustable walking speed.
Read the surface
surface.valuesSeconds is a flat row-major array of elapsed seconds starting at the northwest corner. For width w, cell index is y*w+x. Null means no finite value was retained; numeric zero is valid. Given bounds [west,south,east,north], cell centers are:
longitude = west + (x + 0.5) / width * (east - west)
latitude = north - (y + 0.5) / height * (north - south)
fullSurface.valuesSeconds uses fullSurface.bounds and the same dimensions, with bounds recomputed from reached street evidence when available. Those bounds can be smaller or larger than the requested view. Never interpret full values using the requested bounds. areas/fullAreas are GeoJSON FeatureCollections of MultiPolygons; contours/fullContours contain MultiLineStrings. Features carry cutoffMinutes. Areas preserve holes and disconnected components; isolines interpolate the raster rather than inventing a convex hull through unreachable space. Raster resolution affects presentation and area estimates. See Reach output for decoding and interpretation.
Map output
For map display, Reach accepts reachFormat: "map". It returns the full reached-area polygons as areas, their bounds, and the usual query metadata, without stop lists, duplicate contour representations or raster values. The polygons match fullAreas from reachFormat: "full", which remains the default for analysis and raster comparisons. Output selection does not change the search.
Street evidence
Street evidence is surface.edges with schema vigo.standalone.street-edges.v2, encoding indexed-json. It retains partial directed intervals, with coordinates and times decoded as shown under Reach output. This encoding differs from Studio's binary edge bundles.
Test service changes by applying a scenario to a Reach query. Each scenario lasts for that request; it leaves the City and later requests unchanged.
A scenario object accepts id, name, optional matching cityRevision, excludedTripIds, excludedRouteIds, services, or overlay. Use exact active City identifiers. An exclusion only affects matching scheduled trips.
A supplied cityRevision must match the loaded City. Choose either nonempty services or overlay. Editor-only IDs/fields are rejected; hydrate branch edits into explicit schedules before using this interface.
At least two ordered points; maximum 256 unique planned points across a query
scheduleMode
frequency for add/augment; preserve-trips for replace
sourceRouteId
Required for route-wide frequency replacement when explicit trip IDs are absent
bidirectional
true for frequency; false for preserved trips, where true is rejected
startMinutes, endMinutes
300 and 1500; 0–4319; end must not precede start
headwayMinutes
12; 0.1–1440
timeModel
estimate-distance, preserve-scheduled, or infer-road
averageSpeedKph
22; 1–300
dwellMinutes
0.35; 0–60, used with distance estimates
segmentRuntimeMinutes
One nonnegative number per consecutive stop pair
segmentDistancesKm
One nonnegative number per consecutive stop pair
addedStopDwellMinutes
0; 0–10, applies to inserted/added destination stops with supplied runtimes
scheduledTrips
Explicit trip list for preserve-trips mode
At most 128 services and one million planned stop events are accepted. The default estimate-distance model uses projected stop distance, speed, and dwell; merely supplying runtime arrays does not select them. preserve-scheduled uses supplied runtimes, falling back to distance estimates where no runtime array exists. infer-road consumes supplied distances/runtimes; the standalone service does not infer them from a road route. Use a documented preparation method for those inputs.
augment retains existing service. Frequency replacement uses operation: "replace", scheduleMode: "frequency", and sourceRouteId; matching active trips are removed. If scheduledTrips is provided on a replacement service, its original trip IDs define the removal set. Added service can operate on a date with no active baseline service.
Offsets are seconds relative to that run's departureSeconds; every array matches the stop count. Permissions are zero/one; omitted permission arrays default to all allowed. Times must preserve chronological arrivals/departures. Replacement tripId must be active. Preserve-trips mode does not manufacture reverse runs or automatically recover the original timetable.
Compiled overlays
Advanced clients can supply scenario.overlay: stops, overlayStopCount, direction offsets/stops/times, service windows/headways, and supplemental transfer arrays from TimetableOverlayManyQueryInput. The stop count must equal the coordinate-bearing stop list and be at most 256. The adapter creates endpoint seeds and destinations; the overlay cannot override those base query fields. Direction stop indices use the overlay-local domain; supplemental transfers use the combined resident-plus-overlay domain. Use the native reference for the exact array types and offset conventions.
Compare two Reach results on the same grid to find where travel times improve, worsen, or become unavailable.
Prepare the comparison
Save baseline and scenario Reach responses, then construct a JSON object with those complete objects under before and after. Do not pass filenames as the two values.
Shell
python3 - <<'PY'
import json
with open('baseline.json') as f:
before = json.load(f)
with open('scenario.json') as f:
after = json.load(f)
with open('comparison.json', 'w') as f:
json.dump({'before': before, 'after': after}, f)
PY
./vigo compare --city ./city --request comparison.json --output change.json
Python here is an optional client utility. The Rust command still requires a City path in this version, even though comparison itself uses saved grids. Both results must declare the same nonempty City revision, bounds, dimensions, and compatible numeric/null raster arrays. Compare does not rerun routing and does not currently compare Route or Matrix results.
Interpret changes
The public result, vigo.compare.v1, reports changes after minus before. counts splits the comparable cells, reachable in both, into faster, slower, and unchanged, and counts newlyReachable and noLongerReachable cells separately. meanChangeSeconds averages comparable cells only, is negative when travel got faster, and is null when no cell is comparable. Request diagnostics: "trace" for the per-cell deltaMinutes grid described in Comparison output; a null delta is not a zero change.
Hold origin, clock/date, walking policy, cutoffs, and grid fixed when interpreting a scenario effect. The comparator does not certify all those experiment settings or compare City revisions.
Supply transit predictions or traffic observations with a query to use them in routing. Your application is responsible for fetching and decoding the source feed.
Each query uses only the snapshot supplied with it. Omission restores scheduled/baseline routing on the next request. Explicit routingDataMode: "scheduled" rejects observation input; "realtime" requires a snapshot or traffic. Without an explicit mode, supplied input selects its corresponding observation path.
Transit snapshot
Transit realtime supports Route only. Use a FULL_DATASET snapshot with tripUpdates. Feed timestamps are Unix seconds, not milliseconds. A record needs sourceFeedTimestamp or the enclosing feedTimestamp, no older than 180 seconds and at most 60 seconds in the future. If present, its own timestamp must also be fresh.
Create a current synthetic delayed-trip request from route.json:
Shell
python3 - <<'PY'
import json, time
with open('route.json') as f:
query = json.load(f)
query['routingDataMode'] = 'realtime'
query['realtimeSnapshot'] = {
'incrementality': 'FULL_DATASET',
'feedTimestamp': int(time.time()),
'tripUpdates': [{'tripId': 'T2', 'delaySeconds': 600}]
}
with open('realtime-route.json', 'w') as f:
json.dump(query, f)
PY
./vigo route --city ./city --request realtime-route.json
For real use, preserve the source's timestamp; do not replace stale provider time with the current clock. The example refreshes time only to make a synthetic test record.
Record field
Meaning
tripId
Exact or unambiguous scoped/local static trip ID; nested trip.tripId also accepted
sourceScope
Optional source identity constraint
startDate
Optional matching service date (YYYYMMDD or dashed form); nested trip field accepted
routeId, directionId
Optional static identity checks
delaySeconds
Integer propagated trip delay
scheduleRelationship
Scheduled or cancellation; canceled/deleted trips are removed
stopTimeUpdates
Ordered prediction records identifying one actual call
A stop update uses integer stopSequence or an unambiguous stopId; both, if present, must agree. Loop calls need a sequence. arrival and departure are separate objects containing integer delay seconds or absolute time epoch seconds. Absolute timestamps require realtimeSnapshot.timezone (IANA name) when the City has no single timezone. SKIPPED removes a call; NO_DATA resets propagated delay. Terminal sequence inference is limited to an identified terminal stop; unknown intermediate calls are not guessed.
Invalid, duplicate, stale, ambiguous, or mismatched records preserve scheduled service and increment diagnostics. A malformed enclosing snapshot fails the request. Omitted past prefixes can be removed only when source clocks establish that the omitted calls are past; their boarding opportunities are removed with them. Cached snapshot validity changes at freshness/source-clock boundaries.
Read diagnostics.realtime for coverage and the numbers of applied, canceled, stale, invalid, duplicate, and unmatched records. A successful route may use scheduled fallback. A fresh prediction is not a verified arrival or a completeness guarantee.
Drive traffic
Drive Route and Matrix accept traffic.observations (aliases segments, edgeUpdates). Supply observedAt (aliases fetchedAt, timestamp), optional ttlSeconds (default 300; range 1–1800), and optional expiresAt. Traffic timestamps accept RFC3339 or numeric Unix seconds/milliseconds. Expired data or observation time more than 60 seconds in the future is rejected.
Each observation identifies directed coordinates (2–512 points), fromCoordinate/toCoordinate, or fingerprint-bound edgeIndices. Choose exactly one effect:
Effect
Domain
closed: true
Make selected directed edges unavailable
travelTimeSeconds
0.01–86400 per matched edge
speedKph
1–200
delayFactor
1–100; aliases factor and multiplier
At most 100,000 observations and 200,000 affected edges are accepted. Geometry is matched to the prepared drive graph. Opposite directions are separate. Overlapping observations retain the largest cost; the adapter does not reduce an edge below baseline travel time. This is one supplied static metric snapshot, not time-varying congestion prediction during the trip.
Raw callers may provide snapshotKey, edgeIndices, and edgeTimeUnits (hundredths of seconds), plus the City drive streetSourceFingerprint in the high-level traffic object. A closed edge has weight 2147483647. Actual weights, not just the caller key, determine cache identity. The raw path does not perform timestamp expiry; its producer must manage observation validity and correct graph identity. Low-level native drive inputs already assume the correct index domain.
Default output is a compact journey or analysis result. See the public result contract for every field, unit, evidence boundary, and migration rule.
--format text|json selects terminal text or JSON; pipes and saved files use JSON by default.
--diagnostics none|summary|profile|trace selects optional detail. Default is none.
Route includes available leg GeoJSON by default; --include-geometry=false omits it. Matrix geometry remains opt-in with --include-geometry. --include-limitations adds dataset limitations.
HTTP accepts the equivalent body fields or ?diagnostics=summary, ?includeGeometry=true, and ?includeLimitations=true.
status: "ok" means a result, not_found means no admissible journey, and error means failure.
meta separates engine version, City revision, request identity, query fingerprint, and measured compute time.
Stream consumers may select --stream-output=detailed after checking that capabilities.output.streamFormats includes detailed. This returns the internal vigo.stream.detail.v1 envelope without constructing the public projection; its trace contains the routing outcome. This format is an unstable wrapper interface. See the public result reference for envelope and error semantics.
The public result contract defines the default output. The rest of this chapter documents the internal trace.
Trace reference (debug only)
The rest of this section documents the internal object under trace, obtained with diagnostics: "trace" or stream --stream-output=detailed. These raw fields are retained for debugging and old research tooling, not the default public schema. All old response excerpts below refer to trace. For native operations the raw result remains the direct response. CLI examples in earlier sections use the clean default; old exact field excerpts are trace excerpts.
Read the outcome first, then the journey or analysis data. Keep units, array ordering, and evidence provenance alongside the values when storing or displaying a result.
Timing and metadata
Successful dispatch of Route, Matrix, Reach, and Native adds these fields. A computed blocked Route also receives them. Error objects use a separate envelope.
Field
Meaning
schemaVersion
vigo.standalone.result.v1 for these query families. Compare uses vigo.standalone.comparison.v1. It identifies the JSON interface, not the executable version.
runtime
rust. Use capabilities or --version for the executable version.
cityRevision
Revision of the City loaded by the process. Keep it with saved results; IDs and graph indices depend on the dataset.
timing.totalMs
Milliseconds inside query dispatch, including on-demand preparation and result construction. Excludes initial City load, process startup, final JSON serialization, HTTP queue/network time, and client parsing.
timing.timetableSource
Transit preparation path: prepared_snapshot for a matching compiled service timetable, or source for SQLite preparation. Retained on later requests that reuse that timetable.
id
Echoed by Stream and worker-dispatched HTTP queries when supplied. One-shot CLI results do not echo it; transport failures can omit it.
warnings
Source-provided routing limitations on some families. May be absent, null, or an array; an empty array does not certify the source dataset.
The successful response is the result object itself, without a surrounding data property. Native is the exception in structure: its low-level object is inside result, alongside operation. JSON member order has no meaning. Accept additional fields; inspect the requested family before requiring fields that only exist on particular variants.
serviceDate and dataMode are included on transit Route results; they are not universal metadata. Reach echoes serviceDate (null for a walk-only query without one). Matrix does not echo the date, points, or requested clock. Store the request beside the response when you need reproducible interpretation.
Response fields
Family
Read next
Route, via, and windows
Route output: clocks, legs, waits, geometry, and variants
Matrix
Matrix output: row/column mapping, nulls, deadlines, and journeys
Reach / isochrone
Reach output: raster cells, GeoJSON, nodes, and directed street arrays
A journey was computed under the requested data and policy
Read legs or segments; display the returned mode
Route status: "blocked"
No admissible journey for that request
Read reason; journey fields can be absent
Matrix cell null
That pair has no finite duration under the request
Keep it null; exclude it from numeric averages
Reach cell null or empty evidence
No value was retained there in this computation
Do not convert to zero or infer a transport failure
An error object
The request or transport failed
Handle the error before reading result fields
Do not require kind or status on every family. Route has status but does not uniformly carry kind; Matrix, Reach, and Compare have kind and no top-level Route status. Native has operation. HTTP /v1/info returns the startup City summary without per-query timing.
CLI failures exit 2 and write a JSON error on stderr:
JSON
{"error":{"code":"invalid_request","message":"serviceDate (YYYY-MM-DD) is required for transit"}}
Public errors use schema: "vigo.error.v1" and status: "error". Transport failures use codes such as unauthorized, request_timeout, query_timeout, and service_unavailable; read the code together with HTTP status. A public not_found Route exits 0 and uses HTTP 200. Stream puts per-line errors on stdout and can continue; errors have no normal result timing or City wrapper. See HTTP status codes for 400, 503, and 504 handling.
A Route result describes one journey, a sequence through via points, or a sampled set of departure choices. Start with status, then select the variant by the presence of segments or choices.
Journey clocks and totals
For the reference fixture route, the following is an exact selection of fields. Legs, diagnostics, and metadata are omitted:
Journey start as minutes since the service-day midnight. Depart-at transit starts at the requested clock; arrive-by starts at the selected latest departure.
arrivalMinutes
Journey end on the same service-day clock. An arrive-by transit journey can finish before its deadline.
durationMinutes
arrivalMinutes - departureMinutes; an elapsed duration, not a clock.
boardings
Number of transit boardings in a materialized transit journey.
transfers
max(boardings - 1, 0) for transit; zero for a direct walk/drive.
walkMinutes
Time assigned to non-ride legs, including modeled access/transfer allowances; not a measurement of physical walking alone.
rideMinutes
Sum of ride-leg durations.
waitMinutes
Remaining journey time after walking and riding. Includes gaps before first boarding and between rides.
departure, arrival
Retained native service-day clocks in seconds on transit journeys.
walkingSeconds, rideSeconds, waitingSeconds
Native counterparts of the three minute totals.
Here 475 is 07:55, 510 is 08:30, and 35 = 0 + 25 + 10. Values can be fractional. Service-day clocks can exceed 1440 minutes: 1500 is 25:00 on the selected service day, not a Unix timestamp. Do not apply modulo 24 hours before retaining the date offset.
Read the leg sequence
Geometric walk legs retain the selected access cost. When that cost includes a station connection, accessCost.street and accessCost.station separate their distances and seconds; the station component includes directed stop IDs and source names. A published transfer minimum is timing evidence, not an interior walking-path witness. stationAccessStatus: "unverified" and streetPathVerified: false retain that uncertainty. streetSegmentVerified describes only the street portion. A source-backed pathway can report stationAccessStatus: "source_path". A fallback line between stops is never labelled a verified OSM walk. unverifiedStationAccessLegs counts unresolved station connections in the returned journey.
legs is ordered from origin to destination. The fixture route contains two zero-duration station-selection walk legs around two rides. Waiting is represented by gaps between legs, not separate wait legs:
Step
Clock
Meaning
Access
07:55 → 07:55
Selected stop A; no physical access walk was requested
Gap
07:55 → 08:00
Five minutes before first boarding
Ride T1
08:00 → 08:10
A → X, ten minutes
Gap
08:10 → 08:15
Five minutes before the next ride
Ride T2
08:15 → 08:30
X → B, fifteen minutes
Egress
08:30 → 08:30
Selected destination B
This excerpt is the first ride, legs[1]. Other properties are omitted:
ride or walk within transit; walk or drive for a direct street route.
from, to
Materialized transit endpoint objects: coordinate, plus stopId and name when tied to a stop. Direct street legs omit these objects.
fromStopId, toStopId
String identifiers when an endpoint is a transit stop. Coordinate endpoints omit these fields.
fromStop, toStop, trip
Nullable indices into the active native timetable. They are not GTFS string IDs.
tripId, routeId, route
Ride identity and source route display metadata. route contains nullable shortName, longName, type, and color; it can itself be null.
boardSequence, alightSequence
Native connection-sequence values for reconstructing the ride. Do not treat them as offsets into stopIds or GTFS stop_sequence.
stopIds
Ride's stop sequence including boarding and alighting stops.
stopCount
Number of stop-to-stop segments: stopIds.length - 1.
coordinates
Ordered [longitude, latitude] pairs. These are JSON arrays, not an encoded polyline or a GeoJSON object.
distanceMeters
Geometry/path length when supplied. Ride distance is derived from the returned shape or stop sequence; it is not necessarily the operator's scheduled distance.
To draw one leg as GeoJSON, use {type: "LineString", coordinates: leg.coordinates} when coordinates are available. Preserve separate legs and their provenance. Zero-length station-selection lines can be hidden in a map without removing their role from the itinerary.
Geometry and evidence
geometrySource
What was returned
gtfs_shape
A source shape aligned and sliced between the selected stops
stop_sequence
A fallback line through stop coordinates, which need not follow the street or track
osm
A walking path found in the prepared street graph
osm_and_station_path
A street path joined to the selected station path
gtfs_pathway
A GTFS pathway transfer, drawn between the located stops
stop_coordinate_fallback
A transfer priced by a GTFS transfer rule, drawn between the stops
station_selection
Selected-stop endpoint connector; no street path was checked for that leg
unverified_transfer
A transfer line with no materialized path, including a link between platforms of one parent station
Only transfers certified from OSM follow a street path; a parent-station link keeps its modeled time and a straight line. These labels apply to materialized transit legs. streetPathVerified: true identifies a graph-verified walking leg; false is explicit for the fallback labels. Ride legs and direct Walk/Drive legs do not use that flag in this version. Missing does not mean false. Graph verification does not establish wheelchair access, station-interior access, or fare eligibility.
Route always materializes geometry. Matrix journeys only add coordinates, geometrySource, and geometry-derived distances when includeGeometry is true. This changes detail, not whether the duration cell is reachable.
Direct, via, window, and blocked variants
Direct Walk/Drive results have a single street leg, distanceMeters, clocks, and transfers: 0. They omit transit totals such as boardings and waitMinutes. A transit request with requireTransitRide: false can return mode: "walk"; display the returned mode rather than the requested preference.
Via routes have ordered segments and a top-level duration spanning them. They do not have one flattened legs array or aggregate transfer/walk/ride totals. Segment objects omit the outer dispatch metadata. No visit duration is added at a via point.
Window routes have the selected journey at the top level and up to five retained alternatives in choices, ordered by arrival. window.searches counts sampled departure times, including unsuccessful and deduplicated searches; it is not the number of choices. Retained choices are not a continuous enumeration of every departure.
Reversing A → B to B → A in the fixture route produces this exact field selection:
no_access means one endpoint has no admissible transit candidates; no_path means no path was found under the query. Direct street arrive-by can return before_service_day if it would require a negative departure clock. Drive may return its native reason. A failed via route returns via_leg_blocked, a zero-based legIndex, and the blocked segment under leg. Do not read duration or geometry without a ready outcome.
A Matrix result is indexed by the request's origins and destinations. Keep those arrays with the result: the response includes counts but does not repeat their labels or coordinates.
Rows, columns, and nulls
The Matrix request uses origins [A, X] and destinations [B, A]. Its exact summary is:
durationsMinutes[1][0] is X → B. Starting at X at 07:55 includes 20 minutes waiting and 15 minutes riding. The A → A cell is null because this transit query requires a ride and the fixture has no returning service. Do not fill the diagonal with zero by assumption.
Rows follow origins and columns follow destinations even for arrive-by. No symmetry is implied. null is an unavailable pair under these inputs, while numeric zero is a valid zero duration. Avoid truthiness checks such as if (duration); use duration !== null and retain the count of excluded pairs when aggregating.
Depart-at and arrive-by durations
For transit, the high-level matrix converts native clocks as follows:
The clocks in these formulas are seconds. With A → B and an 08:40 arrive-by deadline, the result cell is 40 minutes: latest departure 08:00 to deadline 08:40. The included journey actually arrives at 08:30 and has 30 minutes of elapsed journey time. Both values are intentional. The extra ten minutes are slack before the deadline, not another ride or a waitMinutes entry in the journey.
Walk/Drive matrices use their street travel durations. distancesMeters is a second two-dimensional array in the identical order. Transit matrices omit this distance array.
Journey detail
Field / request
Returned detail
Transit includeJourneys: false
journeys: null; durations remain available
Transit includeJourneys: true
Two-dimensional journeys[row][column]; unreachable entries are null
includeGeometry: false
Transit clocks, legs, IDs, endpoints, and stop sequences without line geometry
includeGeometry: true
Geometry and provenance added to materialized transit legs
journeyFormat: "compact"
Exact timed trip/stop witness without full display or walking-evidence metadata; requires journeys and excludes geometry
Direct walking wins in a transit matrix
A mode: "walk" journey with a timed walking leg and walking totals; full journeys include geometry when requested
Walk/Drive matrix
Distances and durations; no journeys property. Journey flags are unsupported.
Materialized transit journey cells have the Route journey fields, but no outer status, mode, or dispatch metadata. Read reachability from the duration cell and null journey entry. Do not parse every cell as a complete Route response.
diagnostics.times remains the native flat array of clocks in seconds. For the same cell, its native offset is row * destinationCount + column. It is not interchangeable with the high-level duration matrix, especially when a direct walk replaces a transit result.
High-level Route and Matrix diagnostics retain native clocks and search counters but set the nested native journeys to null. The complete materialized journeys are returned once in the high-level result. Use the explicit native operation timetable.matrix when raw native journey records are required.
Reach returns transit stop arrivals, a raster, area/contour GeoJSON, and optional street evidence. surface.sampling distinguishes reached street samples from routes to grid centers.
Main fields
Field
Interpretation
kind, mode, origin, serviceDate
reach, transit/walk, the requested origin, and the requested date or null
cutoffsMinutes
Sorted, deduplicated elapsed-time thresholds
stops
Transit-reached stops within the largest cutoff, with stopId, name, coordinate, and durationMinutes. Walk-only Reach returns an empty stop array even when streets are reachable.
surface
Grid dimensions, requested/full rasters, and optional street evidence
areas, fullAreas
GeoJSON MultiPolygon features for the requested/full raster respectively
contours, fullContours
GeoJSON MultiLineString features for the requested/full raster respectively
diagnostics
Separate transit-search and street-surface evidence
Stop durations are elapsed minutes from the query clock. They are not clock minutes or final egress times. Stop order follows the active timetable/planned-stop order, not increasing duration. Sort explicitly for a ranked list. Transit stops and raster cells need not have one-to-one coverage because the raster also depends on the prepared street graph.
Decode the raster
surface.width * surface.height equals surface.values.length. Index zero is at the northwest. X grows east and Y grows south. In street sampling, each finite value is the least elapsed time retained from sampled reached street intervals. In cell-center sampling, it is the Matrix travel time to the cell center. Neither promises a path to every point in the cell. Null means no finite value was retained under the query limits.
This client function reads a cell without confusing zero with null:
JavaScript
function readCell(surface, x, y) {
const { width, height, bounds, values } = surface;
if (!Number.isInteger(x) || !Number.isInteger(y) ||
x < 0 || x >= width || y < 0 || y >= height) {
thrownew RangeError('Cell is outside the raster');
}
const [west, south, east, north] = bounds;
return {
index: y * width + x,
coordinate: [
west + (x + 0.5) / width * (east - west),
north - (y + 0.5) / height * (north - south)
],
durationMinutes: values[y * width + x]
};
}
The coordinate is the cell center: a display location for street sampling and the queried destination for cell-center sampling. A cell belongs to a cutoff when its value is not null and is at most that cutoff. Keep the original floating-point value for analysis; round only for display.
For the full raster, pass {...surface, bounds: surface.fullBounds, values: surface.fullValues}. The full pair always exists in the high-level output; it falls back to the requested pair when no separate surface is produced. Both use the same dimensions. Recomputed bounds can be smaller or larger, so equal array indices need not refer to the same place. Changing bounds at fixed dimensions also changes cell size.
Areas and contours
Each collection has type: "FeatureCollection" and a features array. Each feature has properties.cutoffMinutes and one of these geometries:
Geometry
Coordinate nesting
MultiPolygon
coordinates[polygon][ring][vertex] is [longitude, latitude]; ring zero is the exterior and later rings are holes
MultiLineString
coordinates[line][vertex] is [longitude, latitude]
Area thresholds are cumulative, not disjoint bands. A 30-minute feature includes cells also reachable in 15 minutes; do not add their areas together. A cutoff with no generated geometry has no feature, so match features by cutoffMinutes, not by array position. An empty collection is valid.
Areas trace occupied raster cells; contours interpolate threshold crossings. They can differ along boundaries. Both are resolution-dependent representations, not surveyed travel boundaries. Reproject or use geodesic methods for area measurements rather than treating longitude/latitude degrees as meters. Preserve holes and disconnected components when rendering.
Decode directed street evidence
With includeStreetEdges: true, surface.edges uses schemaVersion: "vigo.standalone.street-edges.v2" and encoding: "indexed-json". Without it, edges is null. An included but empty bundle has count zero and empty arrays.
Bundle field
Domain and meaning
count
Number of retained directed interval records
nodeCount, nodes
Local coordinate table; nodes.length = 2 * nodeCount with longitude then latitude
endpoints
Two local node indices per edge: from, then to; length 2 * count
edgeIds
Prepared directed graph edge IDs; length count. These are not local node indices or portable IDs across Cities.
durationMinutes, fromDurationMinutes
Elapsed times at the retained interval end and start
startFractions, endFractions
Interval limits along the directed edge, between zero and one
walkDistanceM
Walking distance accumulated from that label's seed through this edge, including snapping; not the full journey's walking distance or the edge length
transitArrivalMinutes
Elapsed arrival at the transit seed, measured from the query clock. -1 identifies the direct-origin seed; it is not a negative arrival time.
All measurement arrays have length count. Repeated edge IDs describe ordered, non-overlapping intervals; opposite directions are separate records. Interpolate coordinates between the referenced graph vertices using the interval fractions. For a smaller cutoff, clip an interval between its start and end times instead of dropping the whole interval.
JavaScript
function readEdge(bundle, i, cutoffMinutes = Infinity) {
if (!Number.isInteger(i) || i < 0 || i >= bundle.count) {
thrownew RangeError('Edge is outside the bundle');
}
const point = node => bundle.nodes.slice(2 * node, 2 * node + 2);
constfrom = point(bundle.endpoints[2 * i]);
const to = point(bundle.endpoints[2 * i + 1]);
const start = bundle.startFractions?.[i] ?? 0;
const end = bundle.endFractions?.[i] ?? 1;
const startTime = bundle.fromDurationMinutes?.[i] ?? bundle.durationMinutes[i];
const endTime = bundle.durationMinutes[i];
if (cutoffMinutes < startTime) returnnull;
const clippedEnd = endTime <= cutoffMinutes ? end
: start + (end - start) * (cutoffMinutes - startTime) / (endTime - startTime);
if (clippedEnd <= start) returnnull;
const coordinate = fraction => from.map((v, axis) => v + fraction * (to[axis] - v));
const seed = bundle.transitArrivalMinutes[i];
return {
edgeId: bundle.edgeIds[i],
coordinates: [coordinate(start), coordinate(clippedEnd)],
durationMinutes: Math.min(endTime, cutoffMinutes),
walkDistanceM: bundle.walkDistanceM[i],
transitArrivalMinutes: seed === -1 ? null : seed
};
}
Here the client deliberately converts the -1 sentinel to null for display. The API array still contains -1. Retain edgeIds when comparing bundles from the same graph; local endpoints indices can change from one bundle to another. Check diagnostics.surface.edgeEvidenceTruncated before treating the records as exhaustive.
surface.nodes is a separate array of objects with longitude, latitude, durationMinutes, and walkDistanceM; it is not the coordinate table inside edges. With includeNodes: false it is empty. includeNodes: true retains up to 30,000 reached nodes and reports diagnostics.surface.nodeEvidenceTruncated. This diagnostic limit does not truncate the raster or street intervals.
Compare operates on the requested surface.values grids from two saved Reach responses. It does not compare fullValues, GeoJSON areas, stop arrays, or individual street edges.
Signs and reachability
For a cell present in both grids, deltaMinutes = after - before. Negative means faster; positive means slower. The interpretation of null depends on both input cells:
Before
After
Delta
Counted as
Number
Number
After minus before
commonCells
Null
Number
Null
newlyReachableCells
Number
Null
Null
noLongerReachableCells
Null
Null
Null
Neither reachable; no separate returned count
An illustrative two-by-two grid makes the distinction concrete. These values are arithmetic examples, not measured network travel times:
before = [10, null, 20, null]
after = [ 8, 15, null, null]
The comparator returns these exact selected fields for those arrays:
meanChangeMinutes averages common cells only; it is null if there are no common cells. It excludes both new and lost reachability. Never replace delta nulls with zero before averaging. The count unreachable in both is width * height - commonCells - newlyReachableCells - noLongerReachableCells.
Grid and provenance
The response carries width, height, and bounds; deltaMinutes uses the same northwest-first flat order as Reach. Input City revisions and requested grids must match. Fix bounds explicitly when running a baseline and scenario so cell positions stay comparable.
Keep both original requests and responses. The comparator verifies revision/grid/value compatibility but does not verify matching origins, dates, clocks, modes, cutoffs, or walking policies. Its outer cityRevision comes from the City loaded by the process; the input revision check only compares the two saved inputs. Verify those inputs also belong to the loaded City if you rely on the outer revision as provenance.
Diagnostics explain the computation that ran. They are useful for troubleshooting and profiling, but the high-level result fields remain the values to show to clients.
Where diagnostics live
Result
Diagnostic location
Transit Route
diagnostics.originAccess, destinationAccess, search, native, and realtime
Direct Walk/Drive Route
diagnostics is the corresponding native street/drive result
Transit Matrix
diagnostics is the native timetable matrix result
Walk/Drive Matrix
diagnostics is the native street/drive matrix result
Reach
diagnostics.transit and diagnostics.surface; transit is null in walk-only mode
Via Route
Per-segment diagnostics under segments; no aggregate top-level search diagnostic
Transit Route search diagnostics report startSeconds, endSeconds, transfer policy, and horizonScope: "timetable_scan". Endpoint access diagnostics explain selected-stop or coordinate candidate construction. diagnostics.realtime describes an applied supplied snapshot when present; a dataMode label is not an independent prediction-accuracy guarantee.
When a direct walk wins inside a transit request, the top-level route or matrix cell reflects walking while the native diagnostics still describe the transit search. Do not overwrite the returned high-level value with a diagnostic clock or duration.
Timing and counters
queryNs / 1_000_000 converts a native query time to milliseconds. It measures a kernel scope inside dispatch; it is not network latency. Native searches, preparation, geometry materialization, and JSON construction can have different boundaries. Do not sum nested timings or subtract them to infer an isolated cost without checking their scope.
Timetable counters such as scannedDepartures, expandedTripRuns, and relaxedStops count algorithm work, not necessarily distinct trips or stops. Matrix forwardSearches and reverseSearches show execution orientation without changing row/column order. Reach reachedPixels counts finite cells on the requested grid; reachedEdgeCount and reachedEdgeLengthM count directed street edges, so two directions can represent the same physical road. They are not a geographic area measurement or a count of unique road centerlines.
Native result envelope and indices
Native responses contain operation and a typed object under result, plus outer dispatch metadata. The native field reference lists every operation's input and output type. Native result fields are present even when their optional values are null; high-level variant fields can instead be absent.
Timetable operations use indices from timetable.identifiers for the same active service date and policy. stopIds[index] resolves a stop index; tripIds[index] and routeIds[index] resolve a trip and its route. routeIds is parallel to trips, not a deduplicated route catalog. accessMemberStopIds belongs to prepared endpoint-access members, a separate domain. Do not reuse saved numeric indices across changed preparations or timetable activations.
Native timetable.matrix returns a flat origin-major times array of service-day clock seconds. For one A → B pair, the fixture route's departure gives [30600], an 08:30 arrival. Arrive-by gives latest departure clocks instead. journeys is null when not requested; an unreachable numeric slot serializes as null. Street and drive native matrices use flat distances in meters and, for drive, durations in seconds. Read the operation-specific type before interpreting an array named times, values, or distancesM.
Keep a City loaded while sending a sequence of JSON queries through standard input. Streaming returns one result for each request, in order.
One-shot commands open a City for each invocation. stream keeps a City resident and reads one object per line. Every line requires kind; an optional id is echoed even for query errors. Output is one compact JSON object per line, in input order, carrying the request id and a sequence number like the compiler's stream. Blank lines are ignored, and there is no startup handshake.
Transit can reuse a prepared service snapshot only when its source database, access policy, active services, transfer projection, dictionaries, and array layout validate. Missing or invalid optional timetable snapshots fall back to source preparation without writing City files. Realtime and disabled street transfers use source preparation. For repeated calls, keep stream or serve resident; measure fresh-process startup separately from warm query time. The runtime retains immutable shape/alignment data in a bounded cache (1,024 shapes and 64 MiB by default; configurable through VIGO_SHAPE_GEOMETRY_CACHE_MAX_ENTRIES and VIGO_SHAPE_GEOMETRY_CACHE_MAX_BYTES) and shares same-request endpoint evidence even when answer caches are disabled. The shape limit is an upper bound, not reserved memory; it trades a larger possible resident footprint for less repeated geometry preparation.
Full Matrix journeys also share immutable ride and walking evidence within the request, bounded to 4,096 entries and 16 MiB of estimated storage. The key retains the selected endpoint candidates, stop/sequence identities and walking cost; each occurrence keeps its own service clocks. Geometry is unchanged. This storage is discarded after the matrix, including when the query fails. Disabling request caches does not disable this sharing inside one batch.
Route, Matrix, Reach/isochrone, Compare, Native, Info, and Capabilities use the same dispatcher. The process retains the latest active timetable; a different service date is allowed and replaces that timetable. This differs from the older Node stream's fixed-date process. Malformed JSON/invalid queries produce per-line errors and the next line can run. A line exceeding 8 MiB terminates the stream. Stream has no query deadline or supervised restart; use HTTP or your own process supervisor for those guarantees. Close stdin to end it.
Start the HTTP service with vigo serve and send query JSON to the endpoints below. The command line and HTTP API use the same request fields.
Endpoints
Method/path
Response
Authentication
GET /, /docs, /docs/
The offline VIGO guide, including this manual and an API console
Public static content
GET /openapi.json, /docs/openapi.json
OpenAPI specification
Public static content
GET /healthz
200 liveness, status ready/recovering
Public, no City data
GET /readyz
200 ready, 503 while recovering
Public, no City data
GET /v1/capabilities
Runtime features and compatibility
Bearer when configured; otherwise local Host/Origin
GET /v1/info
Loaded City summary
Bearer when configured; otherwise local Host/Origin
POST /v1/route
Route request/result
Bearer when configured; otherwise local Host/Origin
POST /v1/matrix
Matrix request/result
Bearer when configured; otherwise local Host/Origin
POST /v1/reach, /v1/isochrone
Reach request/result
Bearer when configured; otherwise local Host/Origin
POST /v1/compare
Saved Reach comparison
Bearer when configured; otherwise local Host/Origin
POST /v1/native
Typed native operation
Bearer when configured; otherwise local Host/Origin
DELETE /v1/requests/{id}
Cancel a named request
Bearer when configured; otherwise local Host/Origin
The path selects the command; a body kind is overwritten accordingly. Send JSON with Content-Type: application/json. Content-Length and bounded chunked HTTP/1.1 bodies are supported. Duplicate/conflicting length headers are rejected. Connections close after a response; there is no keep-alive, WebSocket, HTTP/2, gzip, streaming response, or browser CORS policy in the service. Other methods/paths return 404 after authentication. A same-origin application or reverse proxy can provide a browser integration policy. Every response carries a restrictive Content-Security-Policy; it allows the embedded guide's API console to call this service's own endpoints (connect-src 'self') and nothing else.
Server limits
The executable runs an HTTP front end and a supervised child worker. The worker keeps one City loaded and processes one query at a time; individual queries can use native parallel kernels. Slow request bodies are read outside that worker. Queue deadlines include waiting and writing to the child. A timed-out or failed worker is stopped and replaced. The service holds no per-client session.
Server option
Default
Allowed range
--host
127.0.0.1
Literal IPv4/IPv6 address; not a hostname
--port
PORT, otherwise 8080
0–65535; zero selects an ephemeral port
--max-body-bytes
8388608
1024–8388608
--request-timeout-ms
10000
50–60000; absolute request read deadline
--query-timeout-ms
30000
1–600000; queued/dispatched computation
--max-connections
32
2–256
--max-queue
8
0–128 waiting jobs
Queued HTTP requests retain encoded JSON; only the worker expands nested query fields. Across connections, uploads and queued requests share a 64 MiB encoded-byte admission budget (including normalization headroom). Chunked uploads reserve their configured maximum before reading. Pending response buffers share a 128 MiB budget until socket writes finish. Capacity exhaustion returns 503, including before 100 Continue for rejected uploads. These bounds cover transport buffers, not total process RSS or the City working set; response reading can additionally hold one worker result of up to 64 MiB. Closing, canceling, or expiring a request releases its reservation when its retained work is discarded.
Waiting routes and matrices of at most 1,024 cells receive up to 32 turns or 250 ms of worker time before the oldest waiting analytical request. FIFO order is preserved within each group, and one admission limit covers both groups. This prevents a submitted batch of Reach jobs from monopolizing the waiting queue. The worker completes its current query before switching; a long running query still contributes to latency.
Headers are limited to 16 KiB and 64 header entries. Worker output is limited to 64 MiB per result. Response writing has a 10-second deadline; startup allows up to 60 seconds for a City worker. Oversized responses or worker exits can appear as 504, so split high-detail matrices/rasters rather than retrying the same oversized query indefinitely.
Cancel a request
A named HTTP request (id containing up to 128 letters, digits, hyphens, underscores or periods) can be canceled through DELETE /v1/requests/{id}. The cancellation returns 202; the original request returns 499. Running work is terminated and its worker replaced; queued work is skipped. Retrying an ID while it is active returns 409.
Status codes
HTTP status
Meaning
200
Computed result, including blocked Route; or successful metadata/documentation
400
Invalid query, JSON, or HTTP framing
401
Missing/incorrect bearer token
403
Without a token: request not addressed to a loopback name, or a browser Origin other than the service's own
404
Unsupported path or method
408
Request read deadline
409
A request with the same id is still active
413
Request body over limit
417
Unsupported Expect header
431
Header byte limit
499
The request was canceled through DELETE /v1/requests/{id}
503
Connection/queue/buffer capacity or worker unavailable; readiness also uses this
504
Query deadline, worker failure, or response-channel failure
The service logs its listening address to stderr. Health routes avoid the computation queue but still share the connection limit. A failed accept or connection setup, such as descriptor exhaustion, drops that connection and the service keeps listening; accept failures pause briefly before the next attempt. If a replacement City worker fails to start, the first failure of each streak is logged with its reason, /readyz returns 503 and queries return 503 while restarts back off exponentially from 250 ms to at most 10 seconds. Use bounded retries with backoff for 503; for 504 first reduce the workload or inspect worker logs. Do not treat a transport error as an unreachable trip.
Send query JSON from your application through HTTP or the command line. An HTTP client needs no VIGO language package.
Use the standalone response fields described in Results and errors. The public Node and Rust result schemas are shared; old internal envelopes are retained only in trace. For reproducible jobs, keep the build identity, capabilities, City revision, complete request, observation timestamps, and output.
A Python standard-library HTTP client needs no VIGO Python package:
Python
import json
import os
import urllib.error
import urllib.request
query = {
"origin": {"stopId": "A"},
"destination": {"stopId": "B"},
"serviceDate": "2026-07-15",
"time": "07:55",
"maxWalkKm": 0.2,
}
headers = {"Content-Type": "application/json"}
if os.environ.get("VIGO_API_TOKEN"):
headers["Authorization"] = "Bearer " + os.environ["VIGO_API_TOKEN"]
request = urllib.request.Request(
"http://127.0.0.1:8080/v1/route",
data=json.dumps(query).encode(), headers=headers,
)
try:
with urllib.request.urlopen(request, timeout=45) as response:
result = json.load(response)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode()) from error
if result.get("status") == "not_found":
print("No journey:", result.get("reason"))
else:
print(result["journey"]["arrivalTime"], result["journey"]["legs"])
A JavaScript client can use fetch with an AbortSignal timeout. Read the HTTP status before interpreting JSON; its successful body can still be a blocked result. Keep bearer tokens in server-side integrations or an appropriate authenticated same-origin service. Do not embed a shared deployment token into publicly served application code.
Call the routing kernels directly when you need packed arrays or controls exposed by a native operation.
Use vigo native --city ./city --request kernel-query.json or POST /v1/native. Supply operation, input, and a service date for timetable operations. Native indices must belong to the loaded City and active service date. The HTTP API never accepts a filesystem path inside a query.
timetable.identifiers returns stopIds, tripIds, routeIds (one per active trip), and accessMemberStopIds. An index is its position in the corresponding array. Resolve each active timetable's IDs before building numeric requests. Cumulative offset arrays start at zero, end at the associated packed-array length, and have one more entry than their group count. Parallel arrays must have equal compatible lengths; indices and times are validated by the kernels.
An ordinary street path uses coordinates and needs no numeric timetable IDs:
Native timetable departure, horizon, earliest, and deadline are absolute service-day seconds. High-level horizonMinutes is a duration; native horizon is an end clock. maximumBoardings counts vehicle boardings, so it equals a high-level transfer cap plus one. Access walk times and transfer arrays are seconds, distances are meters. Drive traffic weights use hundredths of seconds. Native output suffixes such as queryNs retain nanoseconds and must not be relabeled milliseconds.
Native Matrix returns a flat array of clock times in seconds: arrival clocks for depart-at, departure clocks for arrive-by. High-level Matrix returns a two-dimensional array of travel durations in minutes. Both use origin row order and destination column order. Unreachable native times are serialized as null.
timetable.pareto exposes native arrival/boarding/walking trade-offs; it is distinct from sampled departure windows. Native overlays operate on packed caller-supplied arrays. realtime.compile compiles explicit effective call times; it does not fetch/decode a provider feed. The native field reference lists every public operation and nested input/result type from the Rust definitions. Optional request fields may be omitted or set to null. Native result objects include every declared field; unavailable optional values are null. The runtime also enforces semantic limits and compatible array domains.
The complete generated dictionary is in Native Rust field reference. It is also part of the offline guide and the engine archive.
Generated for VIGO 0.5.2 from public Rust structs. Regenerate with npm run docs:standalone.
Use the native operation reference for units, index domains, and examples. In requests, an optional field accepts omission or null. Result objects include every listed field; an unavailable optional value is null. Native arrays become JSON arrays, and non-finite result numbers become null. The runtime also checks compatible array lengths, indices, and values.
Flat origin-major matrix of service-day clocks in seconds: arrivals for depart-at, departures for arrive-by. Null marks an unreachable pair. These are not travel durations.
Vec<f64>
Always
journeys
Journey records in the same order as times. Null when journeys were not requested; individual null entries mark pairs without a journey.
Option<Vec<Option<TimetableMatrixJourney>>>
Always
forwardSearches
u32
Always
reverseSearches
u32
Always
queryNs
Native query time in nanoseconds; excludes outer transport work.
Run the engine executable as the HTTP service. It opens a prepared City or scenario collection without Node, Python, Studio, or an internet connection. Prepare data separately with the compiler, then mount the complete output directory read-only.
Build and check the image
From the repository root with Docker installed:
Shell
docker build -f deploy/Dockerfile -t vigo:0.5.2 .
The image contains the engine with this guide embedded, license notices, and a Debian glibc runtime, and runs as UID/GID 65532. Its compiler matches rust-toolchain.toml; City data, JavaScript dependencies, desktop bundles, and build caches are excluded from the build context. Check the image before using it:
Shell
npm ci
npm run build:cli-runtime
npm run check:deployment -- --image vigo:0.5.2
The check builds public synthetic inputs and exercises the real Compose service: a read-only City, authentication, Route, Matrix, Reach, scenario switching, readiness, restart, and resource limits, and it confirms that queries never change City files. CI runs it on Linux x64 and ARM64. Measure your own City and request mix before choosing production limits; the small fixture is not a metropolitan memory or latency result.
Start the service
Make the City directory and its files readable by UID 65532, and keep every referenced directory inside the mounted City or collection.
Shell
export VIGO_IMAGE=vigo:0.5.2
export VIGO_CITY=/absolute/path/to/prepared-city
# Set VIGO_API_TOKEN through your shell or deployment secret store (16+ characters).
docker compose -f deploy/compose.yml up -d--wait
The service binds to 127.0.0.1:8080 on the host; put an HTTPS reverse proxy in front of it for remote clients. Query endpoints require Authorization: Bearer ..., while readiness and this guide are public.
GET /v1/info reports network, query-workspace, cache, and scenario-residency accounting. For scheduled comparisons, pass scenarioId with each query; a scenario collection stores shared regional data once and keeps a bounded number of scenarios resident. The HTTP API lists every endpoint and status code.
Resource controls
Compose defaults to 2 CPUs, 2 GiB of memory, two native threads, two resident scenarios, and one native query worker with a bounded queue. It uses a read-only root filesystem and City mount, drops Linux capabilities, and caps logs at two 5 MB files.
The worker reuses its network and workspaces, prioritizes short queries, and gives queued analytical work a bounded turn. A deadline or cancellation terminates running work and reloads the worker, and readiness is unavailable during recovery. A full queue returns an error instead of accepting unbounded work. Responses are limited to 64 MiB; split large matrices or omit geometry when appropriate.
GET /healthz checks the HTTP process and GET /readyz checks worker availability. The container runs vigo health against /readyz; the probe loads no City and needs no shell, curl, or Node. The restart policy restarts exited containers, not merely unhealthy ones.
Run without Docker
The engine archive runs directly: vigo serve --city ./city --host 127.0.0.1 --port 8080, checked with vigo health --port 8080. Authentication and hosting covers tokens, reverse proxies, and a systemd unit.
Operate and upgrade
Prepare a new City in a separate directory, verify saved requests against the candidate, and switch the image and City together. Never replace files that a running service has mapped, and keep the previous accepted image and City for rollback; see Upgrade and rollback. Service benchmarking records cold start, first-query and sustained latency, memory, and disk costs. Stop the service with docker compose -f deploy/compose.yml down; the City stays on the host.
Start locally with a prepared City directory. For remote access, configure an API token and put the service behind an HTTPS proxy.
Environment and authentication
The environment variables for the Rust runtime are:
Variable
Purpose
VIGO_CITY
Default prepared City path; overridden by --city
PORT
Default HTTP port; overridden by --port
VIGO_API_TOKEN
Bearer token; nonempty values protect data endpoints, including loopback
RAYON_NUM_THREADS
Optional native parallel thread count
VIGO_MAX_RESIDENT_SCENARIOS
Maximum resident scenario views
VIGO_TIMETABLE_CACHE_BYTES
Inactive scheduled-timetable budget per City or collection; default 134217728 bytes, 0 disables retention
VIGO_ENDPOINT_CACHE_MAX_BYTES
Shared endpoint-cache budget
VIGO_SHAPE_GEOMETRY_CACHE_MAX_BYTES
Shared geometry-cache budget
A non-loopback bind requires a token at least 16 characters long. Set it through your deployment secret mechanism and send Authorization: Bearer .... Without a token, a loopback service accepts data requests only when Host is 127.0.0.1, localhost or [::1] (with an optional port) and any Origin header is that same http:// origin; other requests return 403 forbidden. This blocks DNS-rebinding and cross-site browser requests, as in Studio. Clients that send a different Host, such as a local reverse proxy, should set VIGO_API_TOKEN. The static manual/specification and health endpoints remain public; they contain no query or City data. The token is not a CLI argument and the service does not persist tokens or query bodies.
Shell
export VIGO_CITY=/srv/vigo/city
# Set VIGO_API_TOKEN through your shell or platform secret store.
./vigo serve --host 0.0.0.0 --port 8080 \
--query-timeout-ms 30000 --max-queue 8 --max-connections 32
curl --fail-with-body http://127.0.0.1:8080/v1/route \
-H"Authorization: Bearer $VIGO_API_TOKEN" \
-H'Content-Type: application/json'--data-binary @route.json
Terminate HTTPS at a reverse proxy or hosting platform. Give its upstream timeout room for the chosen query deadline plus request/response transfer. Mount City data read-only and set process/container memory and CPU limits according to measured workloads. Scale with multiple service processes/containers, each with its own resident collection and bounded query queue. There is no globally shared queue, distributed cache, tenant management, or server-side request persistence.
Container
Build the native service image from the repository root:
Shell
docker build -f deploy/Dockerfile -t vigo:0.5.2 .
export VIGO_IMAGE=vigo:0.5.2
export VIGO_CITY=/absolute/path/to/prepared-city
# Set VIGO_API_TOKEN through your deployment secret store.
docker compose -f deploy/compose.yml up -d--wait
The image runs as UID/GID 65532 on Debian glibc, with the native executable and its embedded documentation. Compose mounts the City read-only and defaults to 2 CPUs, 2 GiB, a bounded query queue, bounded caches, and limited log storage. The built-in vigo health command checks worker readiness without loading a City or requiring an external runtime. Give UID 65532 read and traversal access to the City.
For a generic Linux host, install the matching binary and configure an unprivileged service account, readable City directory, and protected environment file. A systemd unit can use:
Set VIGO_CITY and, when desired, VIGO_API_TOKEN in the environment file. Stop the service through the process manager so its worker is also cleaned up. Allow active requests to finish before stopping; queued requests may be interrupted during shutdown.
Python batch concurrency and Engine query concurrency have different costs. City.iter_run(queries, workers=2) overlaps complete queries using separate resident processes. Each owns its prepared City. The native HTTP service uses one supervised query worker with bounded admission, deadlines, cancellation and recovery; its routing kernel can parallelize supported work internally. On a 2 CPU / 2 GiB deployment, keep one resident query process and two native threads. Additional processes are not a free speedup.
The native endpoint-access pool also follows available CPU parallelism (including Linux quota/affinity) and a positive RAYON_NUM_THREADS cap, up to four threads. With one available thread it runs sequentially without creating a pool. This shares one City and preserves exact endpoint searches.
Capacity-aware defaults
At startup, native cache defaults use the smallest reported Linux physical-memory, cgroup v2 memory.max/memory.high, or cgroup v1 memory limit. Visible ancestor limits are included. Unlimited values are ignored. Non-Linux and unknown-capacity installations retain the existing bounded cache defaults. Explicit cache settings remain operator overrides.
Native allocation
Default upper bound
Fraction of detected capacity
Endpoint cache, per role
64 MiB
1/64
Shape geometry cache
64 MiB
1/64
Inactive timetables
128 MiB
1/32
HTTP pending request bytes
64 MiB
1/32
HTTP retained response bytes
128 MiB
1/16
HTTP budgets preserve room for one maximum-sized request and one worker response line. Caches are optional retained work: reducing them does not truncate journeys or change routing results. Collections partition the applicable caches across resident scenarios. The active City and query scratch are outside these cache budgets; a small cache does not prove a large network will fit.
vigo info and /v1/info report resourcePolicy.memoryCapacityBytes and swapCountsAsRam. Capacity is a startup sizing input, not current free memory or a hard process RSS guarantee. Linux controllers at nonstandard mount locations require explicit operator limits. Restart after changing controller limits.
Python caps its existing one-to-four worker budget using affinity, CPU quota and the same visible Linux memory-controller hierarchy. Unknown memory capacity selects one worker. On all platforms, workers=1 remains the public batch default.
Swap is an explicit deployment choice
The Compose service disables swap by default by setting total RAM-plus-swap equal to RAM. To permit up to 4 GiB of swap alongside 2 GiB RAM, use VIGO_MEMORY=2g VIGO_MEMORY_SWAP=6g. The Linux host must already provide sufficient swap; Compose does not create it. A swap-free comparison uses VIGO_MEMORY_SWAP=2g.
Swap is never added to the resident capacity used to size workers or caches. It can preserve cold anonymous pages under pressure, but may substantially increase latency. File-backed mappings can be reclaimed independently. Record RSS, cgroup current/peak memory, swap usage, memory events, pressure and request latency separately; container memory includes page cache and is not RSS. See the Linux memory-controller specification for the accounting rules.
Validation boundaries
The deployment check exercises the shipped image and Compose file at 2 GiB and a 256 MiB synthetic collection, including full geometry, scenario eviction, restart parity and read-only City integrity. The small fixture validates controls and recovery; it does not establish that a regional City fits in 256 MiB. Regional endurance and swap tests need retained workload-specific evidence, including errors and OOM counters, before making a deployment claim.
A resident stream or serve process keeps its City, timetable, and street indexes ready between requests. This chapter describes what it shares, what it retains, and how to account for memory.
Shared data across scenarios
Resident Cities share immutable street arrays and CCH indexes by canonical path. Timetable source columns share equal immutable blocks across resident scenarios. A hash selects candidates, and every reuse checks all values; changes occupy independent blocks without an overlay chain. The collection holds weak references to shared blocks, so evicting their last timetable view releases the data. Stop and trip identities, search indexes, endpoint caches and realtime state remain scenario-specific. The collection still stores separate transit databases and prepared access profiles.
Timetable preparation and retention
Scheduled timetables retain a service-clock prefix through at least 06:00 the next day. Longer requests extend that prefix in six-hour blocks; narrower requests reuse it. Future days are read only through the loaded departure bound. Realtime uses the unfiltered source so an update can move a later trip into the query window. GET /v1/info reports timetablePreparations and timetableCoverageEndSeconds for each resident scenario.
Cold startup opens the City and prepares the first requested service date. Timetable preparation reads trip-sized groups and restores the canonical date order in native arrays; it does not require a new City format or extra disk index. Keep stream or serve resident to reuse this work across requests.
Each standalone City also retains up to eight inactive scheduled timetables, with a default 128 MiB budget set by VIGO_TIMETABLE_CACHE_BYTES. Returning to a retained date and transfer policy reuses its prepared arrays and indexes. Entries use exact dates and sufficient time coverage, preserving calendar exceptions, overnight service and timezone offsets. Least recently used entries are evicted; oversized entries and realtime snapshots are not retained. Set the budget to 0 to disable date retention. This is an additional budget, separate from the active timetable, street data and preparation temporaries. A standalone City receives the full budget. In a scenario collection, retained entries keep their byte allocation and the selected scenario can borrow unused capacity from the collection budget. This avoids a fixed per-scenario share; it does not evict another resident's retained dates to make room. Separate workers each have their own budget. Shared blocks are charged in full for cache admission. memory.timetableCache reports entries, charged retainedBytes, budgetBytes, hits and evictions. Collection residency reports aggregate inactiveTimetableBudgetBytes and inactiveTimetableRetainedBytes. Retained-date activation does not trigger Linux preparation-memory trimming unless retention evicts another timetable. Matrix and Reach still reclaim their temporary allocations after completion. The owner-level memory ledger includes inactive timetables and counts shared blocks once. Calendar rules load once per City as weekday masks plus dated exceptions. Standalone Cities also share equal immutable source-column blocks across retained dates; date-specific scan indexes remain separate. The first-date layout stays contiguous until another date is retained. memory.calendarIndexBytes and memory.timetableSharedColumns expose the index and shared storage.
Query workspaces and residency
One timetable query workspace belongs to the sequential worker and is lent to the selected scenario. Boarding layers, predecessors and generation tags are rebound to its stop and run dimensions; results never cross scenarios. Reusable street-search buffers move with the active request. Exact street attachments for Reach are shared only after scenario-specific station exits are resolved, with a limit of 16,384 coordinates per street graph.
The least recently used City is evicted before opening another when maximumResidentScenarios is reached, retaining the street graph even with a one-City budget. VIGO_MAX_RESIDENT_SCENARIOS overrides this count from 1 to 16; this bounds resident scenarios, not total bytes. GET /v1/info reports current residency, unique timetable block bytes, column-view bytes and query-workspace bytes without opening every scenario. Timetable sourceArrayBytes measures logical column contents, including shared values; do not sum it as exclusive memory.
Memory ledger
The collection's memory ledger separates network allocations, query workspaces, caches, SQLite allocations and mapped file lengths. Its trackedHeapBytes counts shared street and timetable owners once. Each City's memory.ledger provides the component breakdown. Vector capacities are counted; hash-table and JSON-container overheads are estimates. Mapped file lengths are not resident memory. Record process-tree PSS/anonymous memory and cgroup file cache alongside the ledger; active-query temporaries, serialized responses and the application UI have separate lifetimes.
Set VIGO_MEMORY_ACCOUNTING=1 for a diagnostic run to include the last query's raw/public result-tree capacities and their overlap. This walks result objects and is disabled by default; use a separate run for latency measurements.
Node routing workers in one process share admitted immutable street snapshots and CCH files, while keeping mutable query state separate. Admission uses canonical paths and file identities. Weak references release retired networks; replacing a prepared file gives new readers a new owner. Prepared files must remain immutable while mapped. Studio GTFS/OSM import and merge run in disposable child processes. Completion reaches the serving process only after the compiler exits, so compiler allocator arenas cannot remain in the server. Preparation job records retain the compiler process count, largest process RSS peak and last process exit snapshot.
The Node driving kernel retains typed views of its immutable integer road arrays; callers must not mutate them while the kernel exists. The standalone constructor moves owned arrays. Traffic updates use separate mutable weights and leave the base graph intact. sourceArrayBytes is logical array storage, shared with JavaScript when zeroCopyArrays is true, and must not be added twice.
Caches and responses
The HTTP supervisor forwards the worker's validated JSON bytes without constructing another journey or raster object tree. The 64 MiB response limit still applies. Server-Timing's serialize entry measures supervisor serialization; forwarded results report zero there, and serialization in the query worker remains part of end-to-end HTTP time.
The endpoint and shape cache byte budgets apply to the whole collection: each resident receives a share of the configured budget. The defaults allow 64 MiB for each endpoint role and 64 MiB for shape geometry across all residents. Set VIGO_ENDPOINT_CACHE_MAX_BYTES and VIGO_SHAPE_GEOMETRY_CACHE_MAX_BYTES to change those totals. Street search and isochrone lookup pages are allocated only where a query writes. Path scratch above 32 MiB and each dynamic CCH workspace above 64 MiB are discarded before the next query; the inverse street order is retained separately. Access-profile format 6 uses bitmap ranks and offsets for nonempty buckets, and requires rebuilding earlier prepared profiles. On Linux with glibc, freed temporary pages are returned after timetable preparation, Matrix, and Reach. Measure the working set for your region and workload; timetable data and mapped street pages remain outside the result-cache budgets.
Keep a running application and its prepared City together until the replacement passes your saved requests. A new release does not by itself make an older City compatible or incompatible; the prepared-data contract decides.
Before changing versions
Save the runtime version, package checksum, capabilities output, and City inspection (vigo info --city ./city, or vigo-build inspect --city ./city).
Keep the original GTFS and OSM inputs, their checksums, build options, and any scenario definitions. Prepared data is not a substitute for sources.
Keep representative Route, Matrix, and Reach requests and their expected Results, including your transfer, walking, and arrive-by constraints.
Prepared City policy
Cities built with VIGO 0.5.0 or later are a permanent baseline: later readers open them without rebuilding, and standard writers keep producing Cities the published 0.5.0 runtime can read. The prepared-data contract defines that promise and its release gate.
Cities from before 0.5.0 are not converted. Rebuild them from the original sources into a different output directory:
The runtime checks timetable, transfer, street, access, and accelerator formats separately, so the outer vigo.city.v1 manifest alone is not enough, and editing a format marker cannot migrate contents. Optional query caches may be regenerated from an admitted City. Treat published City files as immutable while a service has them open.
Validate, switch, and recover
Run the saved requests against the candidate through the interface your application actually uses. Check schemas, no-journey outcomes, walking geometry, arrival and departure times, boardings, and resource use, and test a cold start as well as repeated requests. Start a second service with city-next, check readiness, and replay the requests before switching the application endpoint. Roll back by restoring the previous binary and City together, and never overwrite a live memory-mapped City.
Checks you can run yourself
From a source checkout with the documented toolchain:
Shell
npm ci
npm run build:cli-runtime
npm run build:standalone
npm run check:correctness
npm run check:public-results
npm run check:portability
npm run package:standalone
python3 test/check-standalone-package.py
These use public synthetic inputs and cover independent objective verification, difficult routing cases, public schemas, City relocation, and operation from the extracted package. They need no private benchmark files or maintainer service. Platform CI runs the broader release checks; a local pass does not establish that unrun platforms passed. Use the service benchmark to keep comparable latency and storage measurements for your workload.
Identify which stage failed: installation, City build or open, request validation, computation, or interpretation. Keep the failing command and its output before changing inputs. For a version change, follow Upgrade and rollback first.
Check the runtime and City
Shell
vigo --version
vigo capabilities --pretty
vigo info --city ./boston --pretty
vigo-build help route
capabilities describes the running binary and info identifies the City. Neither proves that a date has service or that a route is reachable. Source changes take effect only after rebuilding.
Build or open fails
Symptom
Check
Next step
Node version error
node --version against the declared requirement
Install Node.js 24.18 or newer
Native build or load failure
OS and CPU, the pinned Rust toolchain, and the native linker
Build on the machine type that will run VIGO; executables are not portable, City data is
Output City already exists
Whether this is a new revision or a replacement
Prefer a new output directory; use --replace only for an intended replacement
Missing or corrupt street files
Whether the entire City was copied
Restore a complete City or rebuild; never copy individual databases between Cities
Expected a prepared vigo.city.v1 City
The --city path
Point to the compiler's complete output containing network.json
Missing or stale access or CCH artifact
Whether files came from different builds
Rebuild or recopy the whole City with a compatible compiler
Unexpected SQLite WAL
Whether the build completed
Complete the build and copy a consistent, immutable City
First query slower than later ones
Whether timetable or access preparation ran
Use --prepare-date, and separate preparation from resident query time
City absent from Studio's library
Which interface created it
Studio imports its own projects and cannot open compiler City directories
A request is rejected
Symptom
Check
Invalid or uncovered date
Use an exact local YYYY-MM-DD; inspect calendar.txt and calendar_dates.txt
A flag seems to ignore the JSON
Flags override request fields; remove the flag or the field
Point cannot be resolved
Use an exact, case-sensitive stop ID or [longitude, latitude]; place names are not geocoded
Unknown point field
Write points as {"coordinate": [...]} or {"stopId": "..."}, with optional id and name; the compiler's older {"id", "point"} matrix form is not accepted by the engine
Transfer cap with transit waypoints
Unsupported; omit the cap or change the experiment
The compiler allows 100,000 pairs in 16 MiB, the engine 65,536 pairs in 8 MiB
Piped request appears to wait
--request - waits for one complete object and for stdin to close
Invalid requests exit with status 2 and explain the failure on stderr. If an old --output file exists, a failed run can leave it unchanged; do not mistake it for a fresh Result.
Route returns no journey
A public Route returns status: "not_found" when no journey satisfies the request. Read warnings and quality, then request diagnostics: "trace" for searchLimits and accessAvailability.
Confirm the City revision, date, time, and mode. GTFS time 25:10 belongs to the previous calendar day's service date, not to a new day at 01:10.
Check whether transit is required. A direct walk competes by default; requireTransitRide: true requires a boarding.
Check each endpoint's walking access. A stop that is close in a straight line may lack a verified street path, and outside_selected_budget only proposes a wider check.
Check the horizon, transfer cap, and timetable. For depart-at, the horizon bounds boarding and alighting, not door-to-door duration.
If a changed constraint is justified, save it as a new request and keep the blocked Result as part of the analysis.
Never silently raise walking limits, substitute service dates, or turn blocked durations into zero. Route diagnostics explain the bounded failure categories.
Live reports and routes disagree
Observation
Interpretation
A vehicle is visible but its trip is unavailable to Route
Vehicle Positions do not create timetable service; added-service display and routing admission are separate
Realtime times equal the schedule
Unreported trips keep scheduled times; read admitted and excluded counts and any fallback
A saved snapshot stops applying later
Freshness is checked against the captured clock; a saved observation is not live forever
An alert describes a disruption but the route is unchanged
Text alerts are display evidence and close nothing automatically
Playback shows a vehicle that is not reporting
Scheduled playback is labeled Estimated and interpolates the timetable
Keep the service date, source scope, trip identity, feed timestamp, and record timestamp together. See Network and Routes and realtime routing.
Results or comparisons look unexpected
A Matrix ok status can include unreachable null cells, and arrive-by Matrix durations include destination waiting. Matrix comparison requires identical ordered endpoints and Reach comparison identical grids; means exclude unreachable cells. For a path that appears to jump to a platform, inspect its station-path and coordinate-snap qualifications: a visible line is not evidence of an entrance connection.
Report a reproducible issue
Include the VIGO version, OS and CPU, the failing command, the request, the City inspection summary, stderr, and the full Result if one was returned. Say what you expected and whether it reproduces with the same inputs, and share a minimal redistributable fixture when possible. Review paths, coordinates, credentials, and source licenses before attaching files to a public issue, and use the security policy for vulnerabilities. Keep a reproducible run explains what to retain.
Engine and HTTP service
Symptom
Check and next action
Executable cannot run
Match OS/CPU target; on macOS/Linux retain executable permissions
--city or VIGO_CITY is required
Set a prepared City directory, including for Compare
Expected a prepared vigo.city.v1 City
Point to the compiler's complete output with network.json
Missing/stale access or CCH artifact
Rebuild/copy the whole City using a compatible compiler; do not mix artifacts from different builds
Unexpected SQLite WAL
Complete/checkpoint the source build and copy a consistent immutable City
Unknown stop ID
Use exact scoped IDs from the active City; do not use display names
Route blocked
Check service date/clock, active calendar, direction, endpoint access, transfer cap/buffer, and walk limit
Empty or small Reach
Check largest cutoff, service availability, walk budget, raster extent, and street coverage; distinguish null from zero
Bad comparison grid
Recompute both queries with identical explicit bounds and raster size; preserve other experimental settings
Realtime not changing the route
Inspect diagnostic rejection counts, source time, static trip identity/date, and effective predictions
HTTP 401
Set matching VIGO_API_TOKEN; send exactly one Bearer header
HTTP 503
Read readiness; reduce concurrent work/queue pressure or add measured capacity
HTTP 504
Inspect worker logs; reduce matrix size, geometry, raster detail, or split jobs before increasing deadlines
Browser request fails
Check same-origin/proxy configuration; native service does not implement CORS
Unsupported request field
Check this manual or the OpenAPI document; Studio's internal fields and compiler-only flags are not interchangeable
High memory
City size, worker replication, Rayon threads, concurrent request bodies, and detailed results all contribute
Do not use repeated cached-query timing as a cold-start measurement. disableCache leaves loaded City data, prepared artifacts, OS page cache, and other resident state in place. Measure invocation-to-result or client request-to-response explicitly when reporting those boundaries.
Use vigo capabilities to inspect the executable you are running. The standalone interface has its own request and response schemas; the table below describes its supported operations.
Supported operations
Operation
Modes and coverage
Route and Matrix
Transit, walk, and drive; depart-at and arrive-by
Reach
Transit and walk; raster, polygons, contours, and street evidence
Scenarios
Reach with exclusions, explicit schedules, or compiled overlays
Realtime transit
Route
Supplied traffic
Drive Route and Matrix
Saved comparison
Reach surfaces
Drive Reach and realtime transit Matrix/Reach are not supported. City compilation, Studio/Python response envelopes, fare annotations, fare optimization, and Studio route-preference presentation are outside the standalone interface. Prepare editor branches and automatic retiming as explicit schedules or overlays before querying. Native Pareto operations are available separately.
Platform and build records
The packaged test record reports the macOS ARM64 run. CI defines runtime checks for Linux x64/ARM64, macOS ARM64, and Windows x64; use the completed results for the exact build and target you deploy. Linux containers and Windows binaries were not executed in the recorded local audit. The Docker and systemd examples are deployment recipes, not additional test results.
manifest.json records the target, source commit, uncommitted-change flag, source digest, and file hashes. Packages built from uncommitted source are marked dirty: true and remain development artifacts. Keep the manifest with deployments so a result can be traced to its executable and source.
Run the checks
The adapter tests use public synthetic GTFS/OSM fixtures and compare outputs with the existing public CLI. Separate graph and timetable oracles check the shared kernels. These tests cover modeled routing behavior; source-feed completeness, field arrival accuracy, station accessibility, and deployment capacity require their own validation.
Shell
cargo test --manifest-path native/vigo-routing-kernel/Cargo.toml \
--no-default-features--features standalone
cargo clippy --manifest-path native/vigo-routing-kernel/Cargo.toml \
--no-default-features--features standalone --all-targets -- -D warnings
VIGO_STANDALONE_PATH=/absolute/path/to/vigo npm run check:standalone
python3 test/check-standalone-package.py /absolute/path/to/package.tar.gz
npm run docs:standalone
npm run check:standalone-docs
The documentation check verifies the generated guide and specification, links and contracts, and the executable JSON examples. Packaging validates every archive member and payload hash and reruns the adapter suites against the extracted binary. Node/Python are used for development validation only.
Update the documentation
For maintenance, edit this Markdown source. npm run docs:standalone regenerates the OpenAPI document and the native field reference from the Rust public structs, and npm run docs:build rebuilds the guide that the executable embeds. Check generated files before packaging so the embedded documentation describes the same executable.
The standalone adapter uses VIGO's existing Rust timetable, street, driving, shape-alignment, and surface kernels. This audit corrected differences in the surrounding request handling, result materialization, realtime preparation, and deployment behavior. The supported operations now pass the public-fixture checks below. This is a development build, not a claim of complete Studio compatibility or production capacity.
Corrected behavior
Area
Correction and retained regression evidence
Station selection
Selecting a station or child platform includes prepared station members. Map selections use coordinates. Differential routes cover parent and sibling platforms in both time directions.
Walking
Direct walking uses physical distance/speed. Transit access retains configured padding/overhead. Query walking speed reaches both Reach access and its surface. Tests use two City access policies and three walking speeds.
Route evidence
Results retain active stop sequences, route metadata, source-aligned GTFS shapes, and ride/walk/wait totals. Missing shapes and unverified walking geometry carry explicit provenance. Tests compare selected trips, stops, and geometry with the existing CLI.
Realtime
Freshness, trip identity, duplicates, cancellations, inferred terminal sequences, skipped calls, NO_DATA, and invalid predictions follow scheduled fallback semantics with diagnostics. Cached predictions expire; a source-clock crossing invalidates omitted-prefix fallback. A separate Rust test verifies removal of past-prefix boarding opportunities.
Traffic
Cache identities bind actual raw edge weights, even when a caller reuses its snapshot key. A two-query regression changes weights without changing the caller key.
Scenarios
Frequency timing follows the selected time model, defaults match the public interface, and reverse directions charge added-stop dwell at the correct stop. Tests cover exclusions, distance estimates, supplied runtimes, and frequency replacement. Explicit scheduled-trip offsets and permissions are exercised separately.
Input and output
Conflicting clocks, malformed fields, irrelevant CLI flags, invalid grids, unsafe CCH members, and oversized inputs are rejected. Saved JSON replaces its target atomically and preserves existing permissions.
HTTP lifecycle
Connections, headers, bodies, query queue, and responses are bounded. Header/body deadlines cannot be extended by sending small chunks. Slow bodies do not occupy the query worker. A deadline kills and reaps the worker, including when a full input pipe blocks dispatch. Idle crashes restart without needing a query.
Packaging
A fresh allowlisted directory prevents old output files from entering archives. The manifest hashes every payload file and the Rust source inputs; prior output is retained separately. The archive gate validates paths, member types, hashes, and the extracted executable.
Public validation
Observed locally on macOS ARM64 with Rust 1.97.1 and Node 24.18. Node builds synthetic Cities and runs the comparison interface; standalone queries run with an empty PATH and no Node runtime access.
Check
Observed result
test/check-standalone-parity.mjs
276 cases passed against the existing public CLI
test/check-standalone.mjs
64 queries/checks passed, including immutable City contents and authenticated HTTP
test/check-standalone-http.mjs
18 checks passed, including malformed framing, slow-body timeout, capacity, SIGSTOP, full stdin pipe, worker reaping, and idle-crash recovery
Standalone Cargo unit tests
19 passed, including raster holes/disconnected components and realtime past-prefix behavior
Default Node-feature Cargo unit tests
17 passed
Clippy
Both default and standalone features passed with warnings denied
Existing kernel suites
Native routing, native matrices, and Reach engine checks passed
test/check-routing-accuracy.mjs
Passed the existing synthetic independent-oracle checks for streets, driving, timetable queries, matrices, witnesses, and calendars
The differential suite covers two walking-access policies, both time directions, stop and coordinate endpoints, zero/one-transfer constraints, parent stations, sibling platforms, direct-walk policy, calendar additions/removals, matrices, scenario surfaces, realtime fallback, and source geometry. Route/Matrix time comparisons allow the existing interface's 0.001-minute presentation precision. Reach cells allow 0.00051 minutes because the existing adapter rounds transit seeds before surface construction; null/reachable classification must match exactly.
Agreement between adapters does not independently validate their shared kernels. The existing accuracy suite has separate graph and raw-GTFS enumeration oracles, but those remain synthetic tests. No real-world ETA, source-feed completeness, physical station connectivity, fare eligibility, throughput, or memory-capacity claim follows from these results.
To reproduce in a development checkout, build the existing public CLI and native addon, build the standalone executable, and run npm run check:standalone with VIGO_STANDALONE_PATH set to it. Package with python3 scripts/package-standalone.py; then run python3 test/check-standalone-package.py ARCHIVE.tar.gz. That gate reruns all three adapter suites against the extracted binary. Node and Python are development tools; neither ships in the runtime payload. POSIX worker-stop/crash injection is skipped on Windows.
October 4 follow-up
The table above records the original October 3 audit. An October 4 macOS ARM64 follow-up (Rust 1.97.1, Node 26.7.0 as the fixture/compiler host) passed 69 standalone checks, 63 prepared-data checks, 332 differential/regression cases, 18 HTTP lifecycle checks, 42 documentation/HTTP checks, and 23 standalone Cargo tests. Standalone Clippy passed with warnings denied. The additional cases compare current and expired access witnesses in both time directions, repeated matrix endpoints, cached/uncached requests, and two City access policies. They also verify that missing transit access skips the timetable scan without suppressing a requested direct-walk result. Public fixtures remain synthetic; these counts do not establish production capacity or field accuracy.
Remaining compatibility boundaries
City preparation from raw GTFS/OSM remains the existing compiler. The executable consumes an immutable prepared City.
JSON uses the documented standalone schema. Studio/Python envelope compatibility, fare annotations, and Studio route-preference presentation are not implemented.
Scenario editor branch selection and automatic retiming remain preparation steps. Supply explicit scheduled trips or compiled overlays for those changes.
Realtime transit applies to Route; current public CLI and standalone Matrix/Reach do not provide realtime transit queries.
Saved-result comparison currently accepts Reach surfaces. It verifies City revision and grid compatibility, not every experimental setting.
The executable and container recipe are generic, but this local audit did not execute Linux containers or Windows binaries. Release CI is configured to check the corresponding targets. Public hosting and capacity testing remain deployment work.
The package manifest reports source dirty state. A dirty checkout artifact remains a development artifact even when its local checks pass. Nothing in this audit publishes or releases it.
VIGO Python 0.5.2 gives scripts and notebooks a small set of objects for transport analysis. Build or open a City, run Route, Matrix, or Reach, and retain the Result. Reuse one open City for repeated work.
import vigo
with vigo.open("./boston") as city:
result = city.route(
[-71.11902, 42.37334],
[-71.05524, 42.35227],
depart_at="08:00",
service_date="2026-10-05",
)
if result.status == "ready":
print(result.duration_minutes)
This example requires a City covering central Boston and a feed active on the chosen date. Coordinates are [longitude, latitude]. Transit may choose a direct walk; set require_transit_ride=True when a boarding is required.
Python uses ready and blocked Result statuses and minute-valued travel times. A completed Matrix can contain blocked rows. Invalid inputs and execution failures raise exceptions. Results and exports explains each field and the comparison rules.
The public Engine CLI and HTTP schemas use seconds and different status names. Use Python's documented Result properties in Python applications.
Keep repeated work efficient
Use a context manager, retain the open City, and request Matrix journeys or geometry only when needed. Install the optional speed extra for accelerated response decoding. Use result.iter_rows() to copy only consumed rows. Runtime and performance explains process reuse, timing, and resource lifetime.
Platform wheels include Node, the headless CLI bundle, and the Rust kernel. The optional speed extra adds accelerated JSON decoding. Omit the extra for the dependency-free Python package; the packaged Engine is still included. Python 3.10 or newer is required.
From a source checkout, install the wrapper and select a separately built Engine:
VIGO_RUNTIME can name a complete headless directory or a built vigo.mjs file. A source .mjs needs system Node and its matching native kernel. See runtime selection for complete layouts and Windows usage.
2. Prepare the City
Use a complete City built with Engine 0.5. Keep the entire directory, including its timetable and street files. If starting with raw inputs, follow the Engine Boston tutorial, or build from local files in Python:
Python
import vigo
with vigo.build(
"./boston",
gtfs="./data/MBTA_GTFS.zip",
osm="./data/boston.osm.pbf",
) as city:
print(city.name, city.revision_id)
Build once; reopen the saved City in later sessions. Do not call Build inside a query loop. Rebuild pre-0.5.0 prepared Cities from their original inputs.
3. Run a journey query
Save this as route.py. The example requires Boston street coverage and a timetable active on October 5, 2026. Change the date to one covered by your own feed.
Python
import vigo
with vigo.open("./boston") as city:
result = city.route(
[-71.11902, 42.37334],
[-71.05524, 42.35227],
depart_at="08:00",
service_date="2026-10-05",
max_walk_km=1.2,
require_transit_ride=True,
)
if result.status == "ready":
print(f"Journey: {result.duration_minutes:.1f} minutes")
for leg in result.legs:
print(leg["type"])
else:
print("No journey under the supplied constraints.")
result.export("route-result.json")
Run it with the same interpreter used for installation:
Shell
python route.py
The first query starts the resident process and opens the required data. Further queries in the same with block reuse that process. Exiting the block releases it; returned Results remain usable.
4. Read and reuse the answer
duration_minutes is the selected journey duration. legs contains its timed steps and available geometry. Inspect warnings before using a path in a passenger-facing application. A blocked Result is a valid no-journey outcome; VigoError reports a failed operation.
VIGO has four first-class concepts and three Query families. Build or open a City, optionally apply a Scenario, then retain the Result of a Query.
City -> Route | Matrix | Reach -> Result
^
optional Scenario
City
A City is a compiled mobility model built from GTFS and OSM. Its directory is one atomic unit containing the timetable, street model, and the data needed to open them quickly.
A named City may be rebuilt over time. Each build is an immutable revision. A Result records the revision it used.
city.revision_id identifies the build. city.built_at records when it was created. city.sources records its GTFS and OSM inputs. Moving the complete City directory does not change that identity.
Scenario
A Scenario is an immutable set of changes applied to exactly one City revision.
The shared VIGO model can describe:
proposed transit service changes;
one live transit state;
one supplied traffic state.
Query choices such as a walking limit, departure time, or time cutoff are not Scenario changes. A complete alternative GTFS source is another City revision, not a Scenario. Map geometry used to draw an edit is presentation data, not a transport change by itself.
VIGO checks a selected combination before running it. It never drops an unsupported change and quietly returns the unchanged City.
Python supports planned service changes in Reach and supplied traffic in Drive Route/Matrix. Python live-transit Scenarios, realtime transit Matrix, and realtime Reach are unsupported. Supplied traffic uses the shared native driving kernel and requires an engine that advertises that capability.
city.supports(query) and scenario.supports(query) return a Support value with a reason and available alternative when the selected context cannot execute a combination. This is not a complete data-validation pass; execution still checks points, dates, and runtime inputs. See the Python support table.
Query
VIGO has exactly three Query families.
Route
Find and explain travel between ordered points. Transport mode, depart-at, arrive-by, departure windows, waypoints, and batch requests are Route options.
Depart-at minimizes arrival time, then boardings, then walking. Arrive-by maximizes departure time, then minimizes boardings and walking among journeys arriving by the deadline, then actual arrival.
Matrix
Compute scalar travel time between one or more origins and destinations.
Both depart-at and arrive-by accept up to 100,000 pairs. Fixed departures share one forward scan per unique origin; fixed arrival deadlines share one reverse scan per unique destination. Arrive-by duration runs from the latest departure to the deadline, including destination waiting.
Reach
Compute where the represented network can travel within stated time limits. A Reach Result may be viewed as contours or reached streets.
Reach is not Accessibility. Accessibility requires opportunities such as jobs, population, schools, or healthcare in addition to travel impedance.
Result
A Result is the immutable answer to one Query. Every Result exposes:
status;
Query inputs;
warnings;
timing;
City revision and Scenario name;
export methods.
Compare is an action on two compatible Results. It is not a fourth Query family.
Result status is only ready or blocked. A completed Matrix is ready even when individual rows are blocked. Python-side query validation raises InvalidQuery; unsupported combinations raise UnsupportedQuery; runtime validation and execution failures raise VigoError. Background work separately reports queued, running, ready, cancelled, or error through Job.status.
Compare uses retained values and checks family-specific structure; the caller must align dates, points, modes, and other study assumptions. A valid numerical difference does not establish that two Results answer the same question. See Results for comparison and evidence boundaries.
Time
VIGO keeps these durations separate:
Build: GTFS and OSM become a City.
Open: a City revision becomes ready for queries.
Compute: the selected Query runs.
End to end: caller submission through complete Result.
Repeated Route calls may reuse one open process. The answer is still computed for every call.
The wrapper's timing["endToEndMs"] ends before Result construction and export. Use an outer timer for the complete Python operation; see runtime measurement.
The VIGO command line uses the same nouns: vigo build, vigo inspect, vigo route, vigo matrix, vigo reach, and vigo compare.
vigo capabilities reports the public API version and supported combinations. Python requires Engine 0.5.x with API 1.0, capability schema v3, and trace output. Runtime selection rejects other API versions and capability schemas.
Route and Matrix accept max_transfers=0 for at most one boarding, max_transfers=1 for at most two, and so on through 31. Omit the option (or use None) for no additional cap. It applies to both depart_at and arrive_by and all Matrix shapes. A finite cap with ordered transit waypoints is currently unsupported.
These examples use the public vigo package and a complete City at ./boston. Replace the paths, points, and service date with values covered by your data. They describe scheduled model outputs, not observed journeys. See the reference for all arguments and the walkthrough notebook for an interactive, end-to-end version.
Find a journey
Use Route when you need departure and arrival times, transfers, and legs. An arrive-by request searches for the latest feasible departure. Waypoints are visited in their supplied order.
Python
import vigo
with vigo.open("./boston") as city:
journey = city.route(
[-71.062, 42.356],
[-71.071, 42.361],
waypoints=[[-71.058, 42.349]],
arrive_by="09:00",
service_date="2026-09-04",
max_walk_km=1.2,
)
if journey.status == "ready":
print(journey.duration_minutes)
for leg in journey.legs:
print(leg)
else:
print("No journey satisfies this request.")
journey.export("outputs/journey.json")
journey.export("outputs/journey.geojson")
Transit compares a feasible direct walk by default. Set require_transit_ride=True to require a boarding. Use mode="walk" or mode="drive" for street routing. A finite max_transfers is currently unsupported with ordered transit waypoints. Changing a constraint changes the question; retain it in the study record.
Calculate travel times to a common destination
Use Matrix for an assignment table, travel-time screen, or repeated origin–destination analysis. Give points stable caller IDs so rows remain joinable when their input order changes.
Python
import vigo
homes = {
"home-west": [-71.080, 42.356],
"home-south": [-71.062, 42.340],
}
schools = {"school": [-71.058, 42.349]}
with vigo.open("./boston") as city:
morning = city.matrix(
homes,
schools,
arrive_by="08:30",
service_date="2026-09-04",
max_walk_km=1.2,
max_transfers=1,
require_transit_ride=True,
)
rows = morning.rows
ready = [row for row in rows if row["status"] == "ready"]
blocked = [row for row in rows if row["status"] == "blocked"]
print("ready", len(ready), "blocked", len(blocked))
morning.export("outputs/morning.json")
morning.export("outputs/morning.csv")
afternoon = city.matrix(
schools,
homes,
depart_at="15:00",
service_date="2026-09-04",
max_walk_km=1.2,
max_transfers=1,
require_transit_ride=True,
)
afternoon.export("outputs/afternoon.json")
A shared destination and deadline share one reverse timetable scan. A shared origin and departure share one forward scan. Keep the City open while changing endpoint groups. A request accepts up to 100,000 pairs; the product of origin and destination counts is the relevant size.
For arrive-by Matrix, durationMinutes runs from the latest departure to the requested deadline, including waiting after early arrival. Use include_journeys=True when actual arrival and timed legs matter; include_geometry=True additionally materializes their paths. These options increase output and assembly work. The results guide explains row status, missing durations, and nested journeys.
Compare a service proposal
Use the same Reach Query for the baseline and proposed service. Here the proposal adds a modeled service between two coordinates: both directions, a 05:00–25:00 service span, ten-minute headways, estimated travel at 22 km/h, and 0.35 minutes of dwell. These are authored assumptions, not observed operations. Use the Engine's Scenario reference for adding, augmenting, replacing, or excluding service.
Reach's radius bounds the computed area; increasing it changes the analysis, not just the map zoom. Keep the grid, date, time, walking assumptions, and origin aligned. Compare checks grid compatibility but does not establish that all study assumptions match. Report newly reachable and no-longer-reachable cells beside duration changes among comparable cells. Reach alone does not measure jobs, people, or other opportunities.
Run independent queries concurrently
For independent school matrices or other batches, use bounded concurrency while keeping complete Results in input order:
Python
from contextlib import closing
# queries is an iterable of vigo.Route, vigo.Matrix, or vigo.Reach objects.with closing(city.iter_run(queries, workers=2)) as results:
for index, result in enumerate(results):
result.export(f"outputs/query-{index}.json")
The iterator limits pending queries and retained responses. It avoids accumulating the entire batch; city.run(queries, workers=2) can instead return a list when queries is a sequence and all Results fit in memory. The same methods work on a Scenario. No itinerary fields are removed or approximated.
Start with two workers and measure your own workload. Each worker owns a resident Engine process and prepared City. Effective concurrency is capped by CPU/memory capacity (at most four); startup and additional memory can outweigh gains for small batches. Errors surface in input order and stop iteration; already submitted queries may complete. Closing waits for active queries. The default workers=1 preserves serial execution. See the execution reference for the full contract.
Retain every batch outcome
city.run([query, ...]) executes sequentially and stops at the first exception. Use an explicit loop when a study must preserve each success, blocked outcome, and query failure. Opening the City happens outside this loop: a setup failure is a failure of the batch setup, not one blocked journey per request.
Python
import json
from collections import Counter
from pathlib import Path
import vigo
requests = {
"morning": vigo.Route(
[-71.062, 42.356], [-71.058, 42.349],
depart_at="08:00", service_date="2026-09-04",
),
"evening": vigo.Route(
[-71.058, 42.349], [-71.062, 42.356],
depart_at="17:00", service_date="2026-09-04",
),
}
output = Path("outputs/batch")
output.mkdir(parents=True, exist_ok=True)
outcomes = []
with vigo.open("./boston") as city:
for request_id, query in requests.items():
try:
result = city.run(query)
except vigo.VigoError as error:
outcomes.append({
"id": request_id,
"outcome": "query_error",
"errorType": type(error).__name__,
"message": str(error),
})
continue
result.export(output / f"{request_id}.json")
outcomes.append({"id": request_id, "outcome": result.status})
(output / "outcomes.json").write_text(
json.dumps(outcomes, indent=2) + "\n", encoding="utf-8"
)
print(Counter(row["outcome"] for row in outcomes))
Keep the authored requests with this output, including failed requests that have no Result. An export failure remains an exception rather than becoming a routing outcome. Do not replace blocked durations with zero or drop failed requests from the denominator.
For background work, use city.submit(query). A background job can finish successfully with a blocked Result; Job.status describes execution, while Result.status describes the modeled answer.
A Result is the retained answer to one Query. Read its outcome before interpreting its numbers, and keep the query and City revision with any exported table or map.
Outcomes
Outcome
Meaning
How to retain it
Route or Reach ready
A journey or surface was produced under the request
Keep values, assumptions, warnings, and timing
Route or Reach blocked
The computation completed without a usable journey or surface
Keep the outcome; do not turn missing values into zero
Matrix row ready
This pair has a modeled travel time
Analyze the row's duration
Matrix row blocked
This pair has no usable answer under the request
Keep the pair in outcome counts
Exception
Setup, validation, support, timeout, or execution failed
Keep the exception separately from blocked outcomes
A completed Matrix's top-level status is ready even when some or all pairs are blocked. Inspect matrix.rows. A ready Result may still carry warnings; it does not establish that the source data represent every real route, restriction, or operational condition.
Background Job.status is separate. ready means the job returned without an exception; the returned Result can be blocked. See errors and jobs.
Shared fields
Python property
Meaning
kind
route, matrix, reach, or comparison
status
ready or blocked
query
Normalized runtime request, using Engine field names such as serviceDate
warnings
Warnings returned by the runtime
timing
Runtime and wrapper timing fields, where reported
city_revision_id
City build used by the Query
scenario_name
Selected Scenario name, or None
record
Compact record with City revision, Scenario name, query, outcome, warnings, and timing
value
Journey, Matrix rows, Reach surface, or comparison change
Properties such as query, rows, legs, and timing return copies. Editing them does not change the retained Result. to_dict() returns the complete Engine payload; record is a compact Python record, not an alias for that payload. Comparison Results have a different envelope and do not include the two full source Results.
Route
Use duration_minutes for the selected journey and legs for its detail. A blocked Route normally has no duration and no legs. Export JSON for the complete journey; GeoJSON contains only legs with line geometry and a small set of properties.
Depart-at minimizes arrival time, then boardings and walking. Arrive-by maximizes departure time and then resolves secondary objectives within the arrival deadline. A departure-window query is a different question from an exact departure; retain the window with the answer. See the Route reference for constraints.
Matrix
Rows contain caller IDs (originId, destinationId), endpoint indexes, status, and durationMinutes, with other fields depending on the mode and requested detail. Output uses origin-major order. Prefer IDs to row position when joining analysis tables. Use unique IDs and keep their associated coordinates or stop IDs stable across runs.
For arrive-by transit Matrix, departMinutes is the latest feasible departure and arriveMinutes is the requested deadline. durationMinutes includes destination waiting after an early arrival. With include_journeys=True, each ready row's journey supplies actual arrival, transfers, walking, waiting, ride time, and timed legs. With include_geometry=True, that journey also includes selected path geometry. Blocked rows have no journey.
Equal-objective Matrix journeys can differ from Route in which tied path they choose, and therefore in ride/wait splits. The parent Matrix timing covers the shared computation. A nested journey's timing does not represent an independent timetable search or an allocated share of batch time.
Reach
value contains the travel-time surface; to_geojson() returns contours. The grid size and bounds belong to the computation. extent_radius_km limits the area computed around the origin, and raster_size controls its resolution. A contour is a representation of this model, not a guarantee that every location inside a polygon has identical access.
Reach measures travel impedance. An accessibility measure additionally needs opportunity data and an aggregation rule, such as counting jobs below a travel-time threshold. Keep those external inputs and assumptions separate from the Reach Result.
Compare completed Results
vigo.compare(before, after) and before.compare(after) compute differences without running either Query again. The sign is after minus before: a negative duration change is faster.
Family
Comparison behavior
Route
Duration and transfer change when both Results are ready; otherwise the corresponding change is None
Matrix
Match rows by origin/destination IDs, falling back to indexes; count faster, slower, unchanged, newly reachable, and no-longer-reachable pairs
Reach
Require equal grid dimensions, bounds, and value counts; count the same changes across cells
For Matrix and Reach, meanChangeMinutes uses only matched pairs or cells with finite values on both sides. It is None when none are comparable. Newly reachable and no-longer-reachable outcomes are reported separately; they do not enter this mean.
The caller must align the study. Compare does not require equal City revisions, dates, times, modes, walking limits, or other Query settings. Route can compare unrelated endpoints unless the caller prevents it. Matrix silently skips unmatched pairs; repeated IDs can collapse the before-side lookup. Keep unique, stable IDs, check the complete pair sets, and report coverage as well as speed changes. The Reach grid check alone does not establish matching inputs.
Export
Extension
Supported Results
Contents
.json
All
Complete Engine payload
.geojson
Route, Reach
Route leg lines or Reach contours
.csv
Matrix
Row fields
result.export(path) creates parent directories, writes the selected representation, and returns its absolute Path. It overwrites an existing destination. Python exports write directly; use distinct run directories when preserving earlier evidence. to_json(indent=None) returns compact JSON.
Keep JSON as the complete record. GeoJSON omits most query and runtime detail. CSV does not preserve the Result envelope; nested journey objects become text in cells, so JSON is preferable when journeys are requested. Matrix does not expose to_geojson(), even when its nested journeys include geometry.
Retain a reproducible record
Alongside the full Result JSON, retain the authored request or script, complete City revision, Scenario inputs, selected runtime version, and wrapper version. The normalized query is useful but is not a promise to echo every input: for example, a supplied traffic snapshot is not fully reproduced in every query envelope. A Scenario name alone does not reproduce the change.
Python
import json
from pathlib import Path
import vigo
with vigo.open("./boston") as city:
result = city.route(
[-71.062, 42.356], [-71.058, 42.349],
depart_at="08:00", service_date="2026-09-04",
)
result.export("outputs/route.json")
record = {
**result.record,
"wrapperVersion": vigo.__version__,
"engineVersion": city.runtime.product_version,
"apiVersion": city.runtime.api_version,
"sources": city.sources,
}
Path("outputs/record.json").write_text(
json.dumps(record, indent=2) + "\n", encoding="utf-8"
)
Timing describes execution, not accuracy. Synthetic integration tests check contracts and controlled routing behavior; they do not establish field prediction accuracy. Interpret results within the Engine's data and routing limits.
Route and Matrix Results can be separated into journey summaries and individual legs. For Matrix, request include_journeys=True when computing the Result. The helpers use only the Python standard library; pandas is optional for analysis:
This retains blocked journeys and missing itinerary detail in the summary table. Missing component durations stay missing; journey duration remains the Engine value because elapsed waiting and leg durations need not sum to the same total. Leg types and time units retain their Engine meanings. Request include_geometry=True on iter_legs() to copy available geometry.
The input is a saved result.export("result.json") file. The script writes journeys.csv, legs.csv, and record.json with query, warnings and timing. Nested fields are JSON encoded in CSV. Supply --scenario and --city-revision if that provenance is absent from the exported payload. An empty leg table contains no itinerary records. result.export_tables("tables") discovers columns in one pass and writes records in a second, keeping only one record at a time. The Engine response itself is already resident. Serialization completes before existing files are replaced; use a separate output directory per Result when readers need a consistent set of tables.
Reuse processes across dates
With an Engine advertising per-request service dates, one idle process can serve weekday and weekend queries. The pool grows only when requests overlap, within its existing process limit. Older compatible engines retain date-specific processes. Reuse one open City and close it after the study; no process management is required in analysis code.
vigo.open(path) reads the City manifest, checks its required files, and resolves a runtime. It does not load the complete routing network. The first Query starts its runtime process and opens the required native data.
Installation choices
Install
Engine requirement
Use it for
Platform wheel with bundled runtime
Included Node, headless bundle, and native kernel
A self-contained Python environment
Source checkout or universal development wheel
Select a compatible external Engine
Wrapper development or an independently managed Engine
Python with a complete Studio installation
Select the app or extracted distribution
Reusing the Engine shipped with Studio
Python requires version 3.10 or newer. Install a local platform wheel with python -m pip install /path/to/vigo-0.5.2-py3-none-PLATFORM.whl, substituting its real filename. The package has no mandatory Python dependencies. tqdm is optional for Build progress.
For a source install, run python -m pip install -e . from the wrapper checkout, then select a compatible runtime explicitly. An adjacent Engine checkout is not automatically selected. Maintainers can stage and build a headless platform wheel.
The self-contained wheel still communicates with a Node process. Rust performs routing kernels; JavaScript handles GTFS row loading, orchestration, and itinerary assembly. It is not a direct Python/Rust extension.
From GTFS and OSM to the first answer
Measure vigo.build() from just before the call until it returns a City. Use local raw files and a new output directory: this includes import, required street indexes, transfers, station-access preparation, and publication of the saved City. It does not include a Query for a particular service date.
Measure the first city.route() separately, and report their combined time when answering "how long until a new city can answer?" The first Query can prepare its active-service timetable and selected ride geometry.
Python
from time import perf_counter
import vigo
runtime = vigo.resolve_runtime() # Runtime discovery is outside these timers.
started = perf_counter()
with vigo.build(
"./boston", gtfs="./mbta.zip", osm="./massachusetts.osm.pbf", runtime=runtime
) as city:
built = perf_counter()
result = city.route(
[-71.062, 42.356], [-71.058, 42.349],
depart_at="08:00", service_date="2026-09-04",
)
answered = perf_counter()
print("raw_build_seconds", built - started)
print("first_query_seconds", answered - built)
print("raw_to_first_answer_seconds", answered - started)
print("status", result.status)
Choose a date covered by the feed. These timers exclude downloads, installation, runtime discovery, printing, export, and closing the City. Record those costs separately when they belong to the intended workflow. The compiler's city.details["timing"]["totalMs"] ends before final publication and is not the complete elapsed time of vigo.build(). Its component stages can overlap.
Build progress
Install tqdm or install this checkout with python -m pip install ".[progress]", then call:
Python
with vigo.build("./boston", gtfs="./mbta.zip", osm="./massachusetts.osm.pbf", progress=True) as city:
print(city.name)
The optional display uses tqdm.auto and shows the latest stage reported by Engine and the elapsed Build time. The timer continues during long stages, including street-index preparation. It does not invent a percentage or remaining time when Engine has not reported a total. Build errors retain Engine's explanation; timeout or interruption stops the build processes and closes the display. Progress defaults to False and does not add work to Route or Matrix calls.
Reopen a built City
Call vigo.build() once for the GTFS and OSM inputs, then use vigo.open() on the saved directory in later sessions. Raw inputs are not needed for querying. Keep the whole City directory when moving or packaging it: it contains the street indexes, prepared station-access state, and retained timetable snapshots.
The current VIGO runtime restores these prepared files instead of repeating their compilation. Node and the packaged runtime share the same portable timetable format. A new set of active services may need one timetable preparation; dates with identical active services share it. The timetable disk cache is bounded, so an evicted pattern needs preparation again. Missing or outdated timetable/access caches are rebuilt from the compiled City. Missing or corrupt required street files need a complete City restored or rebuilt; changing the raw network data requires a new build.
Reopening still reads files and allocates working memory. Reusing one open City also avoids those costs. Result.timing["openMs"] measures runtime preparation the first time each mode is prepared in a resident process, and is zero when that mode is reused.
Select a runtime
Python
import vigo
runtime = vigo.resolve_runtime()
print(runtime.product_version, runtime.api_version, runtime.source)
with vigo.open("./boston", runtime=runtime) as city:
print(city.sources)
Selection follows this order:
The explicit runtime= argument.
VIGO_RUNTIME, then VIGO_APP.
The bundled headless runtime.
Installed VIGO Studio.app locations on macOS.
vigo on PATH.
An invalid explicit setting fails rather than selecting another engine. Once a complete bundled layout is selected, a failed capability check also ends discovery. Sibling source checkouts are not discovered implicitly. Python requires Engine 0.5.x, API 1.0, capability schema v3, City format 1, Result schema 1, the public commands, and advertised trace output. Other API versions and capability schemas are rejected at runtime selection.
Use a path for a complete runtime directory or executable. Use an argument sequence for a custom command; a shell command string is not split into executable and arguments:
Python
runtime = vigo.resolve_runtime("./headless")
with vigo.open("./boston", runtime=runtime) as city:
print(city.runtime.source, city.runtime.product_version)
Selecting a source vigo.mjs file uses system Node. Build its compatible native kernel and CLI together; the maintainer build steps show the required Engine commands.
Platform wheels include a private Node 24.18.0 executable, vigo.mjs, the native kernel, license notices, and a checksum manifest. Supported build targets are macOS 14+ ARM64/x64, Linux ARM64/x64 with glibc 2.39+ (Ubuntu 24.04 builders), and Windows x64. A wheel must match the target OS and CPU. Source installs and universal development wheels need an external runtime. No import-time or query-time downloads occur.
A headless runtime directory can also be passed to runtime=. Keep its files together. To use Studio as the runtime, select a complete macOS .app, extracted Studio distribution, or packaged executable with its resources directory. Bundled/headless child processes inherit only OS paths and locale settings; provider secrets, Node injection options, and foreign kernel overrides are excluded. Explicit external commands retain the caller's environment.
Successful capability checks are retained for up to eight runtime identities. Each lookup checks the command files' and explicitly selected native kernel's metadata, so replacing or rebuilding the runtime triggers a new check. Failed checks are not cached. Returned capability dictionaries are independent copies. Pass a previously resolved RuntimeInfo to reuse that selection explicitly.
Matrix requires advertised resident execution. Supplied traffic for Drive Route/Matrix requires the corresponding advertised capability; traffic is sent in explicit realtime mode. Transfer caps, arrive-by Matrix, journey output, and transit cache controls also require their advertised capabilities. Unsupported combinations raise UnsupportedQuery; Python validates these options before sending each query to the resident stream.
Keep a City open
With the bundled engine, all Route modes (including waypoints and supplied traffic), Matrix, and Reach share resident processes by service date. A City keeps one to four processes according to CPU and resident memory capacity. On Linux, affinity and cgroup CPU quotas limit concurrency, while cgroup memory limits and memory.high cap the host RAM budget. Limits are read when sizing the executor/City; recreate the City after changing resource limits. Swap is not counted as RAM. Changing dates reclaims the least recently used idle process. Active queries finish before eviction or City.close() releases their process.
VIGO Python 0.5 requires Engine 0.5. All queries use the documented stream protocol. Detailed Python Results request its explicit trace output. Opening several City objects does not share their routing processes.
For repeated work, reuse one City in a context manager. Route answers are recomputed for every call. disable_cache=True on transit Route or Matrix disables street-access and reconstructed walking-path caches while leaving the prepared network resident.
Requests for the same service date share and serialize access to its process. Different dates can use separate processes within the pool limit. submit() moves work to a background thread; it does not create an independent process per request. Await submitted jobs before leaving the City's context.
Timeouts and failures
vigo.open(..., timeout=120) sets the Query timeout in seconds. Waiting for an available resident process and waiting for its response each use that limit. A timed-out resident process is stopped; a later Query can start a fresh one. City.close() waits for active resident queries before closing their processes.
vigo.build(..., timeout=1800) bounds the build command. The City it returns has the default 120-second Query timeout; reopen with a different timeout if needed. Runtime discovery uses a separate 30-second capability-check timeout per candidate. Invalid or unavailable runtimes raise VigoError; Query timeouts raise VigoTimeoutError. These are per-operation limits, not a single end-to-end deadline.
If importing the package exposes none of the documented functions, inspect vigo.__file__ and the active interpreter. A local directory named vigo can shadow the installed package. Install into the intended interpreter and restart the Python session after correcting its import path.
Reduce Python overhead
Install python -m pip install ".[speed]" from this checkout to enable optional orjson decoding directly from resident UTF-8 response bytes, avoiding an intermediate Python string. Without it, Python uses the standard JSON decoder. Request encoding and exports retain strict finite-number checks and existing formatting. Decoding acceleration does not change routing or cache policy. The detailed trace is still requested to preserve Result fields.
Result access copies JSON containers while reusing immutable scalar values. For large matrices, consume result.iter_rows() to avoid copying rows you do not need. CSV export writes stored rows without first copying the full matrix. Use include_journeys=False and include_geometry=False when only travel times are needed; these remain the Matrix defaults. Reuse an open City.
Run python scripts/benchmark-wrapper.py from the checkout to compare the old generic row-copy operation with current Result access and compare standard JSON decoding with the installed decoder. This synthetic benchmark excludes Engine computation, process communication, and City loading. It is not a routing speedup claim.
Measure the complete operation
Record vigo.open() separately from the first Query: most native loading occurs during that Query. Result.timing distinguishes runtime opening and computation where available. For Route, Matrix, and Reach, endToEndMs includes Python request preparation and the runtime response, through payload decoding; Result construction and export occur afterwards. Use an outer wall-clock measurement when including those stages.
Compare first-query and repeated-query durations on the same City, service date, query options, and output detail. Native engineQueryMs excludes parts of access, geometry, communication, and Python work; it is not full-route throughput. Count ready Results, blocked Results, setup failures, and query exceptions separately.
Keep require_transit_ride explicit when comparing results: the current default is False, which allows a direct walk to compete with transit. Set True to require a boarding. Match disable_cache, date, walking budget, coordinates, and output detail before attributing a latency difference to a version change. A sub-millisecond native timetable measurement is distinct from a complete Python Route call.
One City, end to end: build or open, Route (arrive-by and alternatives), Matrix with blocked pairs, journey and leg tables, Reach and GeoJSON, a Scenario comparison, parallel and outcome-keeping batches, and a reproducible study record
Substitute your platform wheel's filename, or use an editable install with VIGO_RUNTIME from a source checkout. If the notebook imports a different environment from your terminal, compare sys.executable and vigo.__file__.
The defaults use the Boston City from the Engine quickstart and Monday 2026-10-05. Configure it with environment variables before starting Jupyter:
Variable
Default
Meaning
VIGO_CITY
./boston
A complete City directory; built here if it does not exist
VIGO_GTFS, VIGO_OSM
None
Raw inputs, needed only to build a missing City
VIGO_SERVICE_DATE
2026-10-05
A date covered by your feed's calendars
VIGO_PLACES
Boston points
JSON object with harvard, kendall, south-station, and copley as [longitude, latitude]
VIGO_OUTPUT
./outputs
Where Results, tables, and the study record are written
Results are scheduled timetable models, not observed journeys. Keep the City open for the session and close it at the end, as the notebook does.
The script writes journeys.csv, legs.csv, and record.json. For Matrix legs, the original query needs include_journeys=True.
Contribute an example
Clear outputs and execution counts, then run python scripts/check-notebooks.py from python/. The check validates notebook structure, compiles every cell, and confirms the walkthrough still covers its documented steps; it does not execute routing. Before changing the walkthrough, run it against a real City, such as the synthetic fixture described in contributing.
Install the package and runtime using the README. Examples use a complete City; substitute its exact stop IDs, coordinates, and covered service dates. For complete scripts, use Workflows.
A City is one complete directory built from GTFS and OSM. It moves as one unit. vigo.open(city, *, runtime=None, timeout=120.0) returns a City; paths may be strings or path-like objects. timeout controls Query execution in seconds, not total context-manager lifetime.
Python
import vigo
city = vigo.open("./boston")
Use a context manager when you want VIGO to release resident runtime resources immediately:
Python
with vigo.open("./boston") as city:
result = city.route(
"place-pktrm",
"place-dwnxg",
depart_at="08:00",
service_date="2026-09-04",
)
print(result.status, result.duration_minutes)
Coordinates always use [longitude, latitude]:
Python
with vigo.open("./boston") as city:
result = city.route(
[-71.062, 42.356],
[-71.058, 42.349],
mode="walk",
depart_at="08:00",
service_date="2026-09-04",
)
city.path, name, revision_id, built_at, and sources identify the opened model. details returns its manifest; capabilities returns the selected runtime's advertised capabilities. city.runtime is the resolved RuntimeInfo. close() releases resident processes; already returned Results remain usable.
Build a City
vigo.build(output, *, gtfs, osm, private_access="public", street_modes="walk,drive", replace=False, runtime=None, timeout=1800.0, progress=False) writes one complete City and returns it opened for queries. gtfs accepts one ZIP path or a sequence of paths; osm accepts one OSM PBF path.
Python
with vigo.build(
"./boston",
gtfs=["./mbta.zip"],
osm="./massachusetts.osm.pbf",
) as city:
print(city.name, city.revision_id, city.built_at)
print(city.sources)
Use street_modes="walk" for a walking/transit-only City; the default "walk,drive" also prepares driving data.
Pass replace=True only when you intend to replace an existing City. Pass progress=True to show Build stages and elapsed time with the optional tqdm dependency. It defaults to False; see Runtime and loading for installation and separate raw-build, first-query, and reopening timings.
For travelers authorized to use internal roads at their own origin and destination, pass private_access="endpoints" to vigo.build(). The default is "public". The endpoint model includes mapped private access walks within the walking budget; the public middle of a journey and transit transfers cannot use private shortcuts. It assumes endpoint authorization and does not infer ownership or gate hours. The runtime must advertise endpoint access before Build.
timeout here bounds the build command. The returned City uses the default 120-second Query timeout; reopen it with vigo.open(..., timeout=...) when longer Query limits are needed. Runtime discovery and capability checks have their own limits. See Runtime and loading.
Points and time
Input
Example
Meaning
Stop ID
"A" or {"stopId": "A"}
Exact ID in this City; no name lookup or geocoding
Structured stop
{"stop": {"id": "A", "feed": "feed-id"}}
Public Engine stop reference; omit feed for an unambiguous stop
Coordinate pair
[-71.062, 42.356]
Longitude, then latitude
Coordinate mapping
{"coordinate": [-71.062, 42.356]}
Explicit point object
Matrix point set
{"home": [-71.062, 42.356]}
Caller ID mapped to a point
Matrix point sequence
["A", "B"]
Auto-generated IDs such as origin_1 and origin_2
Point mappings use the Engine fields stop, stopId, or coordinate. Use [longitude, latitude] or a coordinate mapping for coordinates. Python does not interpret stop_id, lat, lon, or lng as point aliases.
A numeric pair is one coordinate point, not two Matrix endpoints. A mapping with a point key such as stop, stopId, or coordinate is one point, not an ID-to-point set. Use descriptive, unique caller IDs for Matrix sets. The complete Engine point contract describes stop access semantics.
Time arguments accept HH:MM, datetime.time, or datetime.datetime. A datetime supplies its date unless an explicit service_date overrides it. Datetime timezone information is not converted; seconds are omitted. Pass a clock already expressed in the feed's local service time.
String clocks support service-day hours through 29:59. For example, depart_at="25:15", service_date="2026-09-04" means 01:15 on the following civil day within the September 4 service day. Changing the date to September 5 changes which service calendar is selected. service_date accepts an ISO date string or datetime.date and remains required for all modes unless supplied by a datetime.
Route
Opt-in alternatives use alternatives=3 (0–9), alternative_ranking="pareto" or "ranked", and alternative_slack_minutes=15 (0–120). Ranking and slack require a nonzero alternatives count; omitted values use engine defaults. Alternatives require a runtime advertising canonical detailed alternatives and depart-at transit without waypoints or a departure window. result.value stays the primary journey; result.choices returns independent copies of the engine-selected choices, and result.alternative_search retains search coverage and completion metadata. iter_journeys() and iter_legs() still describe the primary Route; use choices or full JSON to inspect alternatives.
Transit Route and Matrix compare a feasible direct walk by default. Use require_transit_ride=True to require a vehicle boarding, or mode="walk" for a walking journey. horizon_minutes sets the transit search horizon (1–2880 minutes; default 480).
Route includes point-to-point, depart-at, arrive-by, departure-window, walking, driving, and batch use.
Argument
Default
Meaning
origin, destination
Required
Exact stop IDs, coordinate pairs, or point mappings
waypoints
Empty
Intermediate points in the given order
mode
"transit"
"transit", "walk", or "drive"
depart_at / arrive_by
One required
HH:MM, datetime.time, or datetime.datetime
service_date
Required unless time is a datetime
ISO date string or datetime.date; interpreted on the feed's local service day
max_walk_km
1.2
Each transit access and egress walking budget
departure_window_minutes
0
Depart-at window of plus or minus 0–30 whole minutes
objective
"earliest_arrival"
The supported objective
max_transfers
None
Optional cap, 0–31 changes
require_transit_ride
False
Require at least one vehicle boarding
horizon_minutes
480
Timetable search horizon, 1–2880 minutes
disable_cache
False
Disable transit street-access and walking-path caches
alternatives
0
Additional Route journeys requested, 0–9; opt-in
alternative_ranking
None
Engine default pareto, or ranked
alternative_slack_minutes
None
Engine default 15; arrival slack from 0 to 120 minutes
Choose departure or arrival time, never both. A nonzero departure window applies to depart-at only.
City.run(query) and Scenario.run(query) are the execution primitives. city.route(...), city.matrix(...), and city.reach(...) are convenience constructors with exactly the same behavior. A sequence executes in input order and returns a list; it stops at the first exception. Use an explicit batch loop to retain failures individually.
City.iter_run(queries, workers=1) and Scenario.iter_run(...) accept any iterable and yield complete Results in input order. Set workers=2 to overlap independent queries in resident processes; run(sequence, workers=2) returns those Results as a list. Worker counts must be integers from 1 to 4, and are capped by the City's CPU/memory capacity. A single Query still executes once. The default remains serial.
Parallel iteration consumes at most the effective worker count of queries ahead. It preserves every result field, including journeys and geometry. Exceptions are raised at their input position; no further input is consumed after that exception, but already submitted queries may have run. Closing the iterator cancels queued work and waits for active queries under their normal timeouts. Use contextlib.closing when stopping early. Each resident worker holds its own prepared City, so higher concurrency uses more memory and is not always faster. Older engines without per-request service dates may serialize queries for the same date.
Depart-at minimizes arrival time, then boardings, then walking. Arrive-by maximizes departure time; among journeys leaving at that boundary and arriving by the deadline, it minimizes boardings, then walking, then actual arrival.
Route and Matrix accept max_transfers=0 for at most one boarding, max_transfers=1 for at most two, and so on through 31. Omit the option (or use None) for no additional cap. It applies to both depart_at and arrive_by and all Matrix shapes. A finite cap with ordered transit waypoints is currently unsupported. The runtime must advertise transfer-cap support; Python rejects this option when its capability is absent.
With the bundled engine, Route, Matrix, and Reach reuse resident processes by service date. Route answers themselves are recomputed. See process lifetime for concurrency and resource lifetime.
For transit Route and Matrix measurements, disable_cache=True disables street-access frontier and reconstructed walking-path caches. It keeps the prepared City resident. Python requires the runtime to advertise this control; a missing capability raises UnsupportedQuery. Result.to_json(indent=None) returns compact JSON, including the complete itinerary and geometry.
Use mode="walk" or mode="drive" for street matrices.
The Matrix Result is ready when the computation completes; some or all of its rows may still be blocked. Count row outcomes before aggregating durations. See Matrix results.
origins and destinations accept a point, a mapping of caller IDs to points, or a sequence of points. Matrix shares Route's mode, date, time constraint, walking budget, objective, transfer cap, transit requirement, horizon, and cache control. It defaults to an 08:00 departure when neither time constraint is supplied. Matrix has no waypoints or departure window.
Additional argument
Default
Meaning
walk_speed_kph
4.8
Walking Matrix speed
max_distance_km
None
Optional street distance limit
include_journeys
False
Add timed transit journeys to ready rows
include_geometry
False
Add geometry; requires include_journeys=True
Transit supports both depart_at and arrive_by with one-to-many, many-to-one, or many-to-many endpoint sets, up to 100,000 pairs per request. For a morning school trip, use city.matrix(assigned_homes, {"school": school}, arrive_by="08:30", service_date="2026-07-15"); for dismissal, reverse the endpoint sets and use depart_at="15:00". A common origin and departure share one forward scan; a common destination and arrival deadline share one reverse scan. Other shapes use one scan per unique origin or destination, respectively. Arrive-by rows report the latest departMinutes and the deadline as arriveMinutes; durationMinutes includes waiting after early arrival. Use Route to obtain actual itinerary arrival and legs. This requires a VIGO runtime whose Matrix capabilities include arrive_by. Successive Matrix and transit Route queries on the same City and service date reuse one resident process. Keep the City open while iterating over schools so the network is loaded once. Closing the City releases those processes. Matrix requires a runtime that advertises resident Matrix support; a missing capability raises UnsupportedQuery before execution.
For transit matrices, include_journeys=True adds actual arrival, walking, waiting, ride time, transfers, and timed trip/stop legs to each ready row's journey. Add include_geometry=True to materialize those selected paths with the same renderer as Route, without repeating timetable searches. The runtime must advertise journey support. Equal-objective paths can differ from Route's stable traversal order. The outer arrive-by row still reports the deadline; the nested journey reports actual arrival.
Reach
Reach answers where the modeled network can travel within stated time limits. It does not count people, jobs, schools, or other opportunities.
Reach accepts origin, service_date, and depart_at (default "08:00"). It supports scheduled transit with walking and walking-only Reach. Driving Reach is unavailable.
Argument
Default
Meaning
mode
"transit"
"transit" or "walk"; walking requires advertised runtime support
surface_sampling
"street"
"street" for reachable network edges, or "cell-center" for coordinate cell centers
include_street_edges
False
Include reached street edges in the surface; adds output size
cutoffs_minutes
(15, 30, 45, 60)
Increasing cutoffs, each 1–240 minutes
max_walk_km
1.2
Transit access and terminal walking budget
walk_speed_kph
4.8
Walking speed, 1–8 kph
extent_radius_km
8
Computation radius, 1–40 km
raster_size
96
Grid side length: 48, 64, 96, 128, 192, 256, 384, 512, or 1024
Use the word Accessibility only when a separate analysis combines travel impedance with opportunities.
extent_radius_km bounds the computed area, not just the display. raster_size changes the grid resolution. Keep both fixed, together with the origin and other assumptions, when comparing a baseline and a Scenario.
Scenario
city.scenario(name, *, services=(), without_routes=(), live=None, traffic=None, expires_at=None) returns an immutable Scenario tied to that City revision. It exposes the same run, route, matrix, reach, supports, and submit methods. Query options such as walking limits remain Query parameters.
Drive Route and Matrix, with advertised supplied-traffic capability
live
Unavailable through Python
Combined planned, live, or traffic states
Unsupported
services accepts mappings following the Engine's service change contract. without_routes accepts exact route IDs. traffic is a supplied snapshot; constructing a Scenario does not fetch observations or establish their validity. The runtime validates the snapshot when executing a supported query. Python sends traffic in explicit realtime mode for Drive Route and Matrix; scheduled transit remains separate.
Nested Scenario inputs are copied and frozen. expires_at is an optional datetime checked before execution; expired Scenarios raise VigoError. A Scenario does not alter the City's baseline. Follow the service proposal workflow for a complete example.
Inspect support before running a context-dependent Query:
Python
with vigo.open("./boston") as city:
proposal = city.scenario("Route removed", without_routes=["route-to-remove"])
query = vigo.Reach([-71.062, 42.356], service_date="2026-09-04")
support = proposal.supports(query)
print(support.supported, support.reason, support.available)
Support contains supported, reason, and available. The check reports supported combinations and selected option constraints; it does not fully validate endpoints, service coverage, Scenario expiry, or all runtime inputs. A supported Query can still fail during execution or return a blocked Result.
Result
Every Query returns Result with the same basic shape. Result properties return independent copies; to_json(indent=None) avoids an intermediate full-result copy:
Route Results add duration_minutes and legs. Matrix Results add rows and iter_rows(). Iteration copies only the rows consumed and yields independently mutable dictionaries. The complete Engine response is already in memory; this is not streaming computation. CSV export reads stored rows directly without creating a second matrix. Both Route and Reach support to_geojson(). value exposes the family-specific answer; to_dict() returns the complete Engine payload.
iter_journeys() yields one summary per Route or Matrix row, including blocked rows, without legs or geometry. hasJourney and legCount distinguish omitted itineraries from empty ones. iter_legs(include_geometry=False) yields one record per supplied leg; Matrix queries need include_journeys=True. Both retain native field names and units, add journeyIndex, cityRevisionId, scenario, and resultStatus, and return independent records. Leg records also carry legIndex, row status, and available origin/destination IDs and indices. journeyIndex is local to one Result; add a study ID when combining Results. Reach and comparison Results raise TypeError for these methods.
See Results for fields, export formats, row outcomes, and comparison limits. vigo.compare(before, after) and before.compare(after) return a comparison Result without rerunning the Queries. Matrix comparisons require originId and destinationId on every row and match those IDs rather than row positions. The caller must align the study inputs; comparison does not validate all Query assumptions.
result.export_tables(directory, include_geometry=False) writes journeys.csv, legs.csv and record.json. It discovers optional columns in one pass and writes them in a second without materializing an extra table. Nested fields are JSON encoded; missing values are blank. Empty tables retain their respective journey and leg headers. Serialization finishes before existing export files are replaced, so serialization errors leave an earlier export intact. Use a separate directory per Result when concurrent readers need a consistent multi-file snapshot.
Result.status is only ready or blocked. A blocked Result is a valid computation with no usable journey or surface. Cancellation and execution failures belong to Job.status or exceptions, not Result status.
Job
city.submit(query) or scenario.submit(query) returns a Job. Pass one Query or a sequence; wait() returns one Result or a list respectively. Background submission keeps the calling Python thread available; it does not promise parallel queries for the same service date.
Keep the City open until its submitted work finishes:
Python
with vigo.open("./boston") as city:
query = vigo.Matrix(
{"home": [-71.062, 42.356]}, {"school": [-71.058, 42.349]},
depart_at="08:00", service_date="2026-09-04",
)
job = city.submit(query)
result = job.wait()
Job.status is queued, running, ready, cancelled, or error. A ready job may contain a blocked Result.
Job.cancel() cancels queued work only. A running Query finishes normally. Job.wait(timeout=...) bounds the wait for the background result; it does not cancel the Query. See Runtime and loading for process reuse and timing boundaries.
Errors
Exception
Meaning
InvalidQuery
Python-side query validation failed; also a ValueError
UnsupportedQuery
A combination or option is unsupported; .support contains the reason and alternatives
VigoTimeoutError
A runtime operation exceeded its timeout; also a TimeoutError
VigoError
Base class for VIGO failures, including runtime validation, incomplete Cities, execution failures, and the subclasses above
Python constructor misuse can also raise ordinary TypeError or ValueError; export can raise filesystem errors. Job.wait() re-raises the background exception. Its own wait timeout raises TimeoutError without cancelling the Query. No exception is converted into a blocked Result.
Runtime information
vigo.resolve_runtime(runtime=None, *, verify=True) returns RuntimeInfo with product_version, api_version, source, path, command, and capabilities. Use the default verification for Query execution. verify=False only resolves a candidate command and returns no verified capabilities; it is not a compatibility check.
A runtime argument can be a RuntimeInfo, a path to a supported runtime layout or executable, or a sequence of command arguments. See Runtime and loading for selection, compatibility, and platform requirements.
The Python wrapper negotiates the resident CLI’s detailed stream format when advertised. It preserves the same complete Result while avoiding a second public projection. Earlier compatible 0.5 runtimes continue to use --diagnostics=trace.
Concurrent jobs and resource limits
City.submit and Scenario.submit share a process-wide limit of 32 running or pending jobs. When capacity is full, submission raises VigoError; retry after work finishes. A canceled queued job keeps its capacity slot until the executor dequeues it, so repeated submit/cancel cannot accumulate an unbounded queue. Canceling a running job remains unsupported.
A resident stream serializes requests for its service date. Different dates can use the bounded City process pool. The query timeout covers writing to the engine pipe and waiting for its response; a stalled pipe terminates the child and releases its buffers. Waiting for a free stream has its own timeout using the City's timeout value.
The Python stream accepts requests up to 8 MiB of UTF-8 JSON and responses up to 64 MiB. Oversized requests raise VigoError before dispatch; oversized responses discard the worker. Split larger matrices or omit optional journey geometry. Response queues and stderr retention are bounded, and idle reader/writer threads release completed response/request buffers.
Start with the failing layer: importing Python, selecting a runtime, opening a City, executing a Query, or interpreting a completed Result. Keep the original error and request before changing inputs.
If the import has no documented functions, a local vigo.py or directory named vigo may shadow the installed package. Use python -m pip with the intended interpreter, then restart the Python or notebook session. A notebook kernel can use a different interpreter from the terminal.
Explicit runtime=, VIGO_RUNTIME, and VIGO_APP take precedence over a bundled engine. An invalid explicit choice fails instead of trying another engine. Check old environment settings when a platform wheel appears to select the wrong runtime. See runtime selection for the full order.
Symptom
Check and next action
No compatible VIGO runtime
Confirm that the install includes a platform runtime, or select a complete external runtime. Source installs require an external Engine.
Native library cannot load
Check OS, CPU, and platform requirements; keep the Node executable, bundle, and native kernel from the same runtime together.
External .mjs cannot run
A source bundle needs compatible system Node and its native kernel. Prefer a complete headless runtime directory or follow Engine's source-build guide.
Studio installation cannot run as a runtime
Select a complete app or extracted distribution, including its resources directory.
API compatibility succeeds but a feature is unsupported
API compatibility and optional Query capabilities are separate. Inspect city.supports(query) and the runtime's advertised capabilities.
Check the City
vigo.open() needs a complete City directory, not a raw GTFS ZIP or one SQLite file. Move or restore the whole build. Its initial checks cover the manifest and required entry files; later native loading can still detect missing or corrupt data.
If the build destination already exists, use a new directory or intentionally pass replace=True. If inputs changed, build a new revision. An incomplete required street model needs restoration or rebuilding; increasing a query timeout cannot repair it.
Distinguish unsupported from blocked
UnsupportedQuery means a requested combination cannot run. Inspect error.support.reason and error.support.available. Planned service changes support Reach; supplied traffic supports Drive Route/Matrix with the advertised capability; Python live transit is unsupported. supports() checks combinations, not every endpoint, date, or data-dependent constraint.
InvalidQuery covers Python-side request checks. Additional invalid requests can fail in Engine and surface as VigoError; catch that base class at a batch boundary when every query failure must be retained. See the batch workflow.
A blocked journey is a completed model answer. Check, in this order:
The exact service date and feed coverage, including the correct service day for an after-midnight time.
Exact stop IDs or [longitude, latitude] coordinate order and City coverage.
Departure versus arrival semantics, horizon, walking budget, and transfer cap.
Whether a vehicle boarding is required. The default is require_transit_ride=False; a feasible direct walk can win.
For Matrix, inspect each row even when the Result itself is ready. Do not retry blocked queries as if the runtime crashed. Only relax constraints when that matches the intended question.
Diagnose time and comparison surprises
Symptom
Explanation
First query is slower than repeats
vigo.open() does not prepare the complete network; the first query pays runtime and mode preparation costs.
disable_cache=True still gives fast repeats
It disables specific transit street caches, while keeping the network and resident process prepared.
A query times out
Use vigo.open(..., timeout=...) for query limits. Build timeout and Job.wait(timeout=...) control different operations.
job.wait() times out but work continues
The wait timed out; it did not cancel the running Query. cancel() only cancels queued work.
Arrive-by Matrix duration is longer than the journey
The row measures to the deadline; the nested journey can arrive earlier.
Reach comparisons reject two maps
Their computation grids must have identical bounds and dimensions. Match origin, extent, and raster size.
A comparison reports fewer pairs than expected
Matrix comparison includes matched IDs only. Check unique IDs, stable endpoint meanings, and pair coverage.
Keep runtime preparation, computation, Python call duration, and export time separate. The runtime guide describes these boundaries.
Report a reproducible issue
Include the wrapper and Engine versions, OS/CPU, runtime selection method, smallest failing Python request, exact exception or Result status, and whether the failure occurs on the first or a repeated query. Include a shareable synthetic fixture when possible. Describe City inputs and build options without uploading licensed feeds, credentials, private paths, or private study results.
Report Python packaging, argument conversion, process lifetime, and Python exports in VIGO Python issues. Shared transport computation belongs in VIGO Engine issues.
VIGO 0.5.2 focuses Studio on network inspection, routing, and analysis, with embedded documentation, better result saving, and less redundant backend work.
Studio
Open Settings → Documentation to read the searchable Studio/engine guide and CLI/HTTP reference offline, in the current light or dark appearance. The same Markdown sources generate the standalone readers and bundled pages.
The former Ask interface, City agency skills, and their unused tests and evaluation fixtures are removed. Network, Routes, Route, Analyze, model-backed service briefings, and saved City evidence remain available. Saved Ask tabs reopen Network; retained notebook records require no migration.
Save and compare results
Journey details export JSON, GeoJSON, CSV, and readable Markdown. Downloads retain selected alternatives, source geometry, service clocks, and initial waiting separately from journey duration. Reach and City/scenario comparisons retain query settings and reference results. Comparison summaries distinguish faster/newly reached cells from slower/lost cells and keep missing evidence unavailable. Preliminary computations cannot be exported as complete results.
Routing and Reach
Parent-station selections prepare fresh walking evidence for each query, with paired origins and destinations sharing the same native frontier. Route and Matrix retain geometry when station, platform, and map-point selections are interleaved. Station street and pathway costs remain unchanged. Street replacement releases resident routing workers before writing new snapshots, preventing file locks on Windows; new routing requests receive a retry response until the import has finished.
Point queries reject provably later candidates earlier and avoid constructing an intermediate journey that exact capped certification would discard. The earliest-arrival/latest-departure, boarding, and walking objectives remain unchanged. Ordinary Node Route replies avoid passing unused search diagnostics through the public formatter; requested diagnostics remain available.
Reached-street propagation uses an exact monotone queue with a fallback for nonmonotone inputs. Small and fractional walking budgets remain exact, and contours preserve geographic bends and exact-cutoff samples. Repeated schedule diagnostic lookups use a bounded service-specific cache. City activation and preparation timings distinguish setup from query work.
Street import exposes Public and Authorized endpoints pedestrian access. Public remains the default. Authorized endpoints can connect a permitted origin or destination through restricted paths; they do not enable private through shortcuts. Replacing streets clears obsolete endpoint overlays.
Alternative journeys
Route can now return several journeys at the requested time. Send alternatives: 3 (up to 9) with a depart-at transit query. The default pareto ranking returns only trade-offs among arrival, boardings, and walking; alternativeRanking: "ranked" returns the earliest distinct journeys even when another journey is better in every respect, counting each sequence of vehicles once. alternativeSlackMinutes (default 15) bounds how much later an alternative may arrive. Alternatives are off by default and are available in the Rust and Node runtimes, the HTTP service, and the CLI flags --alternatives and --alternative-ranking.
One result from both runtimes
The Rust standalone runtime and the Node runtime now return the same public Route result for the same request and City. Both draw transit lines from the boarding stop through the aligned shape to the alighting stop, report walking connections from free coordinates as endpointConnector, price legs with the same GTFS fare engine, explain not_found access failures, and use one computeScope and queryFingerprint. Summary and profile diagnostics still describe each runtime's own instrumentation.
Compatibility and measurement
Published 0.5 Cities remain readable without rebuilding. API 1.0, City format 1, and Result schema 1 are unchanged. The permanent 0.5 data contract remains authoritative. Existing sources, routing policies, and scenario semantics are preserved.
Performance depends on the City, endpoints, query, output, and runtime. Measure City opening, native search, result construction, and transport separately; see measurement guidance. This release does not make a universal sub-millisecond latency claim.
Studio adds nested Reach travel-time bands, clearer journey cards and route colors, ranked stop search, and refined library recovery and responsive controls.
Public journey output moves completed leg, alternative, and stop values into response containers, avoiding duplicate geometry allocations. Point queries initialize the alternatives cache only when alternatives are requested.
Route alternatives and the primary journey reuse immutable leg geometry and metadata within one search. The cache retains the existing 16 MiB budget and refreshes output clocks for each journey.
Node and standalone Rust Reach area polygons preserve islands inside holes and unreachable cells that touch at a corner. Cell sampling and travel times are unchanged; polygon nesting and ring splitting now preserve the raster mask.
Interpolated contours close at raster edges and no-data boundaries, preserve disconnected samples, and use a consistent cell-center convention. The Reach guide explains how to compare fixed-departure surfaces with R5 and OTP.
GTFS frequency expansion and ride-leg assembly reuse prepared SQL statements. Reach metrics count reachable cells once per cutoff.
Scalar timetable scans skip provably dominated reboarding checks while preserving equal-time closure and ordered journey selection.
Reuse prepared service dates
The standalone Rust runtime retains prepared scheduled timetables when queries switch dates. Returning to a retained date and transfer policy reuses its arrays and indexes, while routing search and its shared query workspace remain unchanged.
Each City retains up to eight inactive timetables within a default 128 MiB budget. Set VIGO_TIMETABLE_CACHE_BYTES to tune the budget, or 0 to disable retention. Least recently used entries are evicted; oversized timetables and realtime snapshots are not retained. The budget is additional to the active timetable and allows the selected scenario to borrow unused capacity in a scenario collection; separate workers retain separate budgets.
memory.timetableCache exposes retention, hits and evictions. See the standalone guide for accounting and configuration.
Compatibility
Published 0.5 Cities open without rebuilding. API 1.0, City format 1 and Result schema 1 are unchanged. The permanent prepared-data contract remains the compatibility baseline.
Reduce preparation and output overhead
First-date preparation reads timetable rows in trip order and restores service-date order with compact trip blocks. This avoids a whole-timetable SQL sort while preserving connection order, boarding permissions and overnight service. Existing Cities need no rebuild or additional disk index.
Timetable indexing borrows source columns instead of duplicating them. Reach reuses raster-crossing scratch across edges while preserving exact grid coverage. Matrix materialization reuses bounded trip metadata within a request. Departure and arrival windows compute candidate ranking and duplicate-detection keys once per selection pass, preserving journey order and alternatives. Linux retains allocator pages on date-cache hits unless preparation or cache eviction released data.
The resident CLI advertises per-request service dates for Python process reuse. Date changes reuse the loaded street network and select a retained timetable. Its service-context cache retains up to four contexts by default within the existing byte budget; VIGO_ACTIVE_KERNEL_CONTEXT_CACHE_MAX_ENTRIES remains configurable. HTTP scheduling reclaims expired and canceled queued requests before dispatch without consuming the short-query fairness allowance.
Calendar views and matrix reuse
Calendar weekday masks and dated exceptions are indexed once per City. Equal immutable timetable columns share storage across retained dates, while each date keeps its scan indexes. Collection cache capacity follows demand within the existing total byte budget.
Transit Matrix reuses repeated OD pairs through direct walking and geometry reconstruction, then restores the requested output order. Trace diagnostics report the requested and distinct pair counts.
The Node CLI also reuses repeated journey materialization, including direct-walk geometry. Its request-local cache is bounded to 2,048 pairs and a 1 MiB key-storage estimate. Returned journeys are independent copies; diagnostics expose materialized and reused journey counts.
Studio responsiveness
Queued point queries, including departure-window alternatives, take priority over analytical work, with at most four bypasses before the oldest analytical job runs. Preparation order and running queries are preserved; existing worker and memory limits still apply.
Reach point inspection applies the map’s walking limit, keeping the selected journey consistent with the accessibility query.
Studio makes journey comparisons easier to scan and uses less memory when driving is not needed. Routing objectives, station access rules, transfer minima, and timetable precision are unchanged.
Studio
Journey cards show total time from the requested departure, transfers, walking, and the tradeoff against the earliest arrival. Sort the returned choices by earliest arrival, fewer transfers, or less walking. Arrive-by choices rank by latest departure. Sorting does not rerun the search. Arrow keys, Home, and End move between choices, with one keyboard tab stop for the list. Closing the itinerary preserves the selected journey on the map. The itinerary header uses the same total-time basis as its card.
Obsolete light-mode style overrides are replaced by shared theme rules. Old alert-scope reconstruction and saved-answer rewriting are removed; current notices use their original selectors and saved answers display their stored text.
Shared surfaces use a quieter palette, consistent control corners, readable journey metrics, and fewer repeated badges. Light and dark themes retain visible focus and reduced-motion support.
Opening a City prepares walking. Driving opens when Drive is selected or a driving operation requires it. A driving request received during walking preparation upgrades the shared job. This avoids retaining a second street network for transit-only work. The replaceable browser cache has a 64 MiB budget; City sources and retained runs are separate.
Data and responses
Prepared snapshot headers use lossless gzip compression for large metadata blocks. Numeric arrays retain their aligned layout for native reading. Access snapshots store repeated transfer evidence and stop records once; Node restores shared records when loading. The source transfer costs, evidence, and directed station paths are preserved.
VIGO 0.5.0 uses one snapshot format: compressed metadata and aligned numeric arrays. The old uncompressed header and access-context readers are removed. Rebuild prepared Cities with 0.5.0 before using the standalone runtime. Studio regenerates rejected derived caches from the imported source stores.
Street hierarchy files use a compact v2 layout: seven aligned query columns, with no stored inverse rank or redundant arc tails. Walking files omit input-arc mappings after customization. Driving keeps those mappings in a losslessly compressed section, read only for traffic updates. Writing uses a 16 KiB scratch buffer instead of copying a full graph column. The v1 hierarchy reader and local-ID serialization layer are removed. Build new standalone Cities from source. Studio prepares a missing current hierarchy from its admitted street snapshot, with progress in the existing preparation job; driving remains on demand.
Transit shapes stay in one native cache. Rust clips each selected leg into a reusable 8 KiB buffer; JavaScript receives only its displayed points. Source-order alignment, endpoint selection, and sampling are preserved. Node and standalone default to a 64 MiB shape cache with at most 1,024 shapes.
Studio returns one response shape that sends each journey once and excludes internal cache inventories. It retains full leg geometry, route times, data warnings, realtime status, and request timing. The internal Studio channel remains separate from the supported Engine API. The hidden _route-stream command and its separate result envelope are removed. Use stream with an explicit kind on every query. VIGO Python 0.5 uses this same protocol and requires Engine 0.5; older-runtime fallbacks are removed. Public Result schemas remain version 1; alternatives exclude duplicate copies of the primary journey. Use includeGeometry: false for smaller point-routing responses and diagnostics: "trace" when the full internal evidence is needed.
Raw City build correction
The corrected builder always writes the required pedestrian accelerator before sealing the source street tables. Missing or unwritable snapshots now fail at preparation instead of producing a City that cannot open. Numeric node lookups, disk-backed SQLite sorting, cgroup-aware scheduling and separate compilation processes reduce retained build memory without changing the published 0.5 data layout. Existing valid 0.5 Cities do not need rebuilding for this correction.
For transit-only preparation, use --street-modes walk. Large regional native preprocessing still needs swap on small machines: the tested MA/NH regional build completed in 462.302 seconds in a 2-vCPU / 2-GiB VM with a 1,500 MiB container RAM cap and 947 MiB peak swap. That is an offline-build result, not a no-swap build claim or a simultaneous build-and-serve measurement.
Deployment and packaging
HTTP deployments use the native vigo serve runtime and one Docker/Compose configuration. The separate Node HTTP service and its Engine ZIP are removed; the Node CLI remains the preparation tool. Compose defaults to 2 CPUs and 2 GiB, read-only City data, bounded queues and caches, and limited logs. vigo health checks readiness without loading a City.
Packaging replaces completed artifacts without accumulating a backup for every build. Failed replacements restore the previous package. The standalone archive verifies that its documentation matches the bytes embedded in the executable. CLI builds publish their output only after bundling succeeds.
Arrive-by reserves now adjust the service-day clock before applying the margin, allowing a buffer to cross midnight consistently in Route and Matrix.
Departure queries retain the cold-store direct-walk proof without opening the full transit store. Public service benchmarks validate the required Route, Matrix, and Reach outputs, distinguish malformed responses from HTTP failures, and apply deadlines through response-body reads. Unrelated aborts and connection resets remain transport errors.
Realtime reconstruction that exceeds the configured memory budget preserves its capacity error and 503 status through the routing worker. The resident scheduled timetable remains usable after the rejected request.
Reach rasterization records every grid cell crossed by a reachable street segment, including brief corner crossings. Studio derives detailed boundaries from the sampled surface and keeps display estimates separate from raw routing evidence. Direct-walking comparisons use the full query time allowance, and completed blocked results no longer appear to be loading.
Reproduce the checks
cargo test --locked --manifest-path native/vigo-routing-kernel/Cargo.toml --no-default-features --features standalone --lib checks compact hierarchy reloads, traffic customization, directed shortest paths against independent enumeration, and corrupt-file rejection.
node test/check-native-materialization.mjs checks shape alignment and clipping against exhaustive reference implementations, including scratch-buffer ownership.
node test/check-prepared-access-compaction.mjs checks fractional costs, transfer evidence, obsolete-format rejection, shared stop records, and size reduction on a synthetic public fixture.
node test/check-studio-route-response.mjs checks retained journey evidence and failure status in compact responses.
node test/check-street-preparation-order.mjs checks walking-only loading, on-demand driving, and concurrent requests.
node test/check-journey-options-runtime.mjs exercises sorting and keyboard selection and renders both themes at desktop and phone widths.
npm run check:deployment -- --image IMAGE exercises the actual service container, read-only City and scenario collection, authentication, queries, and restart.
npm run check:package-publication verifies successful replacement and recovery from interrupted package publication.
npm run check:standalone checks the prepared City format and public results across Node and Rust.
node test/check-national-routing-store.mjs checks the cold-store walking proof and walking comparisons alongside transit routing.
node test/check-reach-geometry.mjs checks boundary topology and conservative display estimates without changing raw samples.
Storage and memory reductions depend on the City and requested output. Hierarchy compression does not change street weights, transit objectives, or station-access rules.
Retired compatibility paths
Cities must use exact-date GTFS service calendars. The former JSON timetable compiler and weekday-template store mode have been removed; rebuild those stores from source GTFS. Current GTFS Cities keep the same format.
Live-feed requests use sources, with explicit endpoint URLs and roles. Top-level url and urls aliases, viewer-URL extraction, and implicit expansion of one MBTA URL into three feeds have been removed. Reconnect old saved live-feed configurations through the feed editor. The explicit MBTA preset remains available.
The release checks now include grouped-feed routing and City data interactions. CLI and Studio share the same routing-result status classifier; unused status badges, status normalization aliases, and styles have been removed.
GTFS import rejects blank stop IDs before publishing a store, including scoped multi-feed builds. Tests no longer preserve that invalid-input behavior. Retired geometry and URL-validation wrappers have been removed; boundary, route geometry, and URL safety checks exercise the current production functions.
Public-source checks scan tracked files for private data, credentials, and machine paths. Dependency checks require public npm and crates.io sources, with local Rust paths confined to tracked source inside this repository. Replay fixtures reference the public VIGO repository.
On macOS, startup and background status checks wait for the user to open the City library before accessing Documents. The default Documents location and existing City files are retained. Health and settings status checks are read-only after opening as well. Invalid folder selections and malformed JSON return 400 without opening the library. macOS builds and release checks now target Apple Silicon only; Intel macOS is no longer a supported target.
Scenario collections now propagate strict wheelchair preparation to every member and the shared street graph. Both build-scenarios --wheelchair and wheelchair: true in the source specification are supported; invalid or conflicting profiles fail before compilation.
Node CLI JSON requests now honor departureWindowMinutes. Standalone-only window fields fail explicitly in Node; the standalone CLI and HTTP service support both departure and arrive-by windows. Public walking alternatives retain their endpoints and do not duplicate a primary walking journey. Station nodes without coordinates retain their IDs, names, and times, while invalid coordinates and incomplete line geometries are omitted. Service validation checks leg endpoints and optional geometry as well as the journey envelope.
HTTP concurrency retains encoded requests instead of one expanded JSON tree per waiting connection. Aggregate upload/queue and response-buffer limits bound transport memory independently of connection counts; overload is reported as 503 and reservations are reclaimed after cancellation, expiry or socket completion.
Dated release and validation records are retained below. They are not current-release instructions.
Studio in use
The VIGO 0.4.2 screenshots and their source/attribution notes remain in the archived visual record. They show the application on 26 September 2026 and are not current UI instructions. Use the Studio guide for the current release.
Architecture audit for 0.4.2
This records the final local VIGO 0.4.2 freeze, including the runtime hardening and multi-feed follow-up. The user-authorized retag replaces the earlier local freeze. Realtime and traffic methods and runtime limits and recovery document current behavior.
This review covers the Engine and Studio repository: import and City storage, native adapters and kernels, worker scheduling, CLI and HTTP boundaries, React and map surfaces, agency research interfaces, documentation, and release checks. The Python wrapper, private comparisons, and manuscript are separate repositories and are outside this freeze. API 1.0, City format 1, and Result schema 1 remain unchanged.
The audit combines source inspection, repository-wide export/reference and duplicate-block scans, and the repository's regression suites. Static scans are candidates for investigation, not proof that every runtime path is reachable or that all defects have been found. The initial architecture review did not run a comparative benchmark. Follow-up performance changes preserve the routing contract; performance measurement defines the evidence needed for a workload-specific speed claim.
Repairs included in the freeze
Finding
Consequence
Repair and regression
Worker message transfer could throw after setting the active job
An immediately rejected request left the worker busy; a queued failure could escape from the completion handler
Catch transfer failure, clean cancellation listeners, reject that job, and continue the queue without restarting the prepared worker. check-national-runtime-isolation.mjs uses actual workers to check clone failure, capacity, shutdown and recovery.
A timetable retained a string-keyed map of every street identity/access-profile projection used with it
Repeated policy changes could accumulate full stop-mapping arrays for the timetable's lifetime
Weak keys for both owners, with one current profile per pair. check-stop-projection.mjs covers reuse, 100 replacements, return to an earlier profile, unknown stops, and distinct owners.
Release checks verified package versions but not the CI tag name
A differently named tag could package and publish the wrong declared version
Tagged CI runs require v followed by the package version.
Engine failures could reject a desktop protocol handler without an explicit response
Unavailable-engine requests depended on Electron's rejection handling
Return a structured 503; the actual-process recovery check verifies this response after exhausting the restart budget.
Matrix reference used implementation vocabulary for returned journeys
Readers needed algorithm context to interpret ordinary output
Replace witness/materialization wording with journey/assembly where equivalent; define necessary algorithm terms in the architecture guide.
Selected-journey geometry copied or traversed more shape data than the returned segment needed
Assembly could dominate the native timetable query
Load numeric shape buffers natively, prune exact alignment candidates, bound range sampling and combine geometry cleanup/distance work. Native materialization and route-geometry checks preserve sampling and route outputs.
Numeric whole-unit fare parsing initialized localized currency formatting
The first fare-bearing response paid a presentation setup cost after the inner route timer ended
Use a conservative exact-integer fast path; retain currency-specific validation for fractional and large amounts. The fare suite checks every runtime-supported currency and integration with imported catalogs.
Runtime and projection regressions run in check:national-runtime, included in check:release. Existing Route/Matrix and lifecycle suites check the integrated behavior. Cache eviction changes preparation reuse, not routing objectives.
Remaining cost centers
These are mechanisms visible in code, not ranked measurements of production latency. Use the timing boundaries before making speed claims.
Layer and source
Cost or constraint
Decision for this freeze
City compilation and GTFS coordinator: src/server/national-gtfs-store.mjs
Import, source validation, transfer construction, and active-service preparation remain substantial first-use work
Preserve atomic publication and identity checks; separate Build, reopen, and resident measurements.
Street preparation: src/server/national-osm-store.mjs, native-routing-kernel.mjs
Opening validates graph arrays and required indexes; older ephemeral Drive Cities rebuild the hierarchy
Retain validation; new City builds already persist Drive structures. Rebuild older Cities when persisted preparation is needed.
Native shape loading and alignment still leave selected-row lookup, range sampling and itinerary-object assembly
Retain the explicit boundary; native search timing alone excludes this work. Fare annotation also occurs outside the inner route timer.
Realtime: src/server/gtfs/realtime-timetable.mjs
A changed or newly invalid snapshot requires reconstruction and indexing
Preserve captured-clock validity and scheduled isolation; measure unchanged and changed snapshots separately.
Renderer and map: src/App.tsx, src/VigoMap.tsx, src/map/
Large feature replacement, geometry decoding, and render effects can dominate visible response
Retain lazy decoding and serialized source updates; interaction checks cover refresh isolation and small layouts.
Follow-up repairs
Studio now connects multiple GTFS-RT endpoints and retains the selected static source on every record. Stop-specific delay no longer propagates backward. Unknown relationships, unordered stop sequences and oversized snapshots are rejected. Individual source failures remain visible through routing coverage. The method audit records the complete findings.
Downloads, decoding, reconstructed timetables, worker queues and restart attempts have finite limits. The actual-process recovery checks replace substituted workers and clocks. No native search or provider response is fabricated.
Cleanup decisions
The initial exported-symbol scan found no single-occurrence exported function, class, or constant candidates across tracked source, tests, scripts, and public entry points. The initial exact normalized 12-line block scan found no cross-file duplicate blocks above its 380-character threshold in source. These limited checks do not detect every unused path or semantic duplicate. TypeScript already enforces unused locals and parameters.
Do not remove the LAMP saved-study reader, replay inputs, model-history filters, or old City compatibility solely because their names look historical. They have retained readers, runtime consumers, or explicit format behavior. Vendored CCH code and its notices remain a separately maintained dependency.
The large GTFS coordinator, API entry point, App component, and native kernels remain maintenance risks. Extract future modules by ownership of store identity, cache lifetime, request state, or search workspace. A line-count-driven split at the freeze would move complexity without proving a behavior improvement.
The follow-up removed 26 redundant test/fixture files and over 6,000 net lines of simulated infrastructure before this final pass. This pass also removes the duplicate URL parser and an enum test loop that mirrored the lookup itself, while extending real multi-source integration and vehicle identity checks. Scratch imports, captures, temporary profiles, logs and superseded archives are removed after verification. Final screenshots and their public source provenance remain in documentation; the final distributable remains under release/.
Freeze verification
Run npm run check:release, npm run check:studio-runtime, Rust tests and Clippy, then npm run release:studio on the target host. Build the printable guide with npm run docs:developer-guide. The release check also verifies source publication boundaries and version consistency. Generated binaries, test logs, and archives stay outside tracked source.
A local tag identifies the reviewed source revision. It does not establish foreign-platform validation, remote publication, signed provenance, live feed accuracy, or performance on an untested City. The release workflow performs the supported-platform builds and attestation when dispatched or triggered remotely.
The initial local freeze on 26 September 2026 on macOS ARM64 passed the full release suite, Studio interaction suite, all 16 Rust tests, Clippy with warnings denied, and the Studio build/package/isolated-runtime/archive checks. The printable guide compiled to 10 pages. Source checks ran on Node 26.7.0, which satisfies the declared minimum; the CI configuration pins Node 24.18.0 and was not rerun remotely in this audit. Matching-tag acceptance and mismatched-tag rejection were both exercised. Other operating systems and CPU targets remain unverified by this local run.
The itinerary and fare follow-up reran the full release suite, Studio interaction suite, production build, all 16 Rust tests, and Clippy with warnings denied. Recovery verification now consumes each health response before deliberately killing its Engine. After initial recovery-test timeouts, the complete Studio suite and three further fresh-process recovery checks passed with that cleanup; production recovery behavior was not changed. The initial timeout cause was not independently isolated.
Realtime and traffic method audit · 0.4.2
This audit covers supported routing behavior in Engine and Studio. It is software verification, not a paper result, a live ETA calibration, or a claim about dispatch outcomes. No traffic provider was configured for this release.
What a realtime answer means
A transit query resolves records against an active static trip and service day, applies accepted predictions to a separate timetable, then runs native search and journey reconstruction. Unreported trips retain scheduled times. The answer can therefore contain a mixture of predictions and schedule-based legs. routingDataMode: "realtime" alone does not establish that a prediction was used.
The audit follows the primary GTFS-Realtime reference and producer best practices. These specifications define feed semantics; they do not certify VIGO's implementation or an individual producer's accuracy.
Findings and repairs
Finding
Repair
Verification
Studio accepted one group of URLs and omitted static source identity during normalization
Explicit endpoint list, timetable selection, source metadata on every record, and one shared URL normalization boundary
Two imported GTFS feeds with identical trip IDs, actual protobuf over HTTP, native forward/reverse queries
A first stop-level delay could be copied into the trip-wide delay
Preserve only the actual TripUpdate delay at trip level; stop predictions propagate forward from their own calls
A downstream prediction leaves origin boarding unchanged; separate literal-timetable comparisons
Unknown enum values could disappear and become the default scheduled relationship
Retain UNKNOWN and reject unsupported routing relationships
Decoder/normalization and routing admission checks
Unordered stop sequences were accepted through lookup maps
Reject non-increasing reported sequences
Timing admission regression
Successful records could appear fully admitted while another endpoint failed
Production bounded-reader/security tests and multi-feed integration
Native realtime reconstruction could allocate beside an already resident scheduled view
Check a conservative reconstruction estimate against the same finite timetable budget before native compilation
Native reconstruction regressions and low-budget runtime admission checks
The existing separate worker and Engine recovery checks use actual thread/process termination and subsequent real routing. They do not claim recovery from every native hang or operating-system out-of-memory condition. See runtime limits.
Semantic checks retained
Question
Implemented rule
Can one agency's trip_id change another?
Only an exact or uniquely resolved source identity can match; contradictory scope, route and direction are excluded
Can a missing report establish on-time service?
No. Scheduled fallback is disclosed; supplied-update coverage is separate from all-service reporting coverage
Can an old cached snapshot stay live?
No. Feed and record clocks are rechecked; expiry changes cache identity
Can predictions travel backward along the trip?
No. Stop-specific delay propagates downstream; absolute time wins over delay; arrival/departure remain separate
Can an update make a journey run backward in time?
Contradictory event order and native numeric overflow are rejected
Can a canceled or deleted trip reappear through fallback?
Removed trips stay removed in both search directions; cancellation does not authorize a different service date
Can skipped stops still be boarded?
No. Through travel remains possible; boarding and alighting are removed
Does NO_DATA continue a prior delay?
No. It resets propagated delay
Do route endpoints restrict which predictions get applied?
No. All admitted supplied updates participate in the rebuilt timetable
Are prediction uncertainty fields confidence intervals?
No. Routing uses point predictions; it does not infer a calibrated probability or arrival interval
FULL_DATASET is supported. Entity deletion markers that require a differential stream are rejected; trip cancellations remain distinct. DIFFERENTIAL and unknown incrementality are rejected. NEW/ADDED, replacement, duplicated and frequency-instance routing remain outside the supported contract; some added service can be displayed separately. A feed header's feed_version is retained but not automatically checked against static feed_info. One timezone per combined routing store remains required. Vehicle Positions and Alerts do not generate arbitrary travel-time costs or transit closures.
Traffic boundary
Drive Route and Matrix accept caller-supplied observed edge costs and closures. They customize the same native graph and reuse unchanged effective metrics. Expired snapshots disclose baseline fallback; invalid direct indices require the correct graph fingerprint. Regressions check slowdown, alternative paths, blocked closures, expiry and Route/Matrix agreement through actual native and worker execution.
This is a static snapshot of road costs. It does not model future traffic evolution, all turn restrictions, signals, or parking. Nearest-edge geometry matching is not a provider-calibrated mapping. No public performance or ETA-quality claim follows from these checks. See the traffic contract and input example.
Efficiency and evidence
Static GTFS import and native index preparation occur at City build/open. Queries reuse prepared graphs, endpoint mappings and the latest compiled realtime view where identities and timestamp validity agree. Duplicate feed URLs are fetched once. Decoded feeds are normalized as they arrive, so a batch need not retain every raw protobuf. Native traffic customization reuses the effective edge metric.
Costs remain in feed decoding, full-snapshot hashing and reconstruction, source matching, selected-journey assembly and map updates. Increasing worker count can multiply graph memory. These mechanisms explain where work occurs; they are not a ranked production profile or a measured speedup.
The retained independent transit oracle compares 1,536 pairs of realtime queries with separately imported literal predicted timetables, plus 16 past-prefix comparisons and 263 exhaustive journey checks. Multi-source integration adds two directions over independently imported duplicate-ID feeds. Constructed inputs test declared semantics; real MBTA/OpenStreetMap application captures test the working integration on those downloaded sources. Neither establishes field ETA accuracy.
Run npm run check:release, npm run check:studio-runtime, and npm run release:studio. The release audit records final platform verification and cleanup. Test inputs are actual GTFS/OSM/protobuf documents; production routing, storage, browser and process code computes the outcomes. No model responses or route-worker implementations are substituted.
VIGO 0.4.3
Final source revision: 2026-10-06, retaining version 0.4.3. This version includes the corrected pedestrian model and requires rebuilding Cities from raw inputs, including Cities built by earlier 0.4.3 candidates. Package manifests identify the exact source revision; deployment validation remains specific to each platform and dataset.
Apply the station-pathway correction consistently through Node and standalone Rust CLI and HTTP Route/Matrix interfaces. npm run check:pathway-cost-model checks both directions, published-time precedence, blocked paths and estimated station-time labels against public synthetic inputs.
Restore declared stair and gate connectivity when GTFS omits optional traversal times. Stair-count and gate estimates retain distinct provenance and unverified physical timing; rebuild the GTFS routing store to use transfer semantics v4.
Correct transfer walking floors, OSM node restrictions, missing station coordinates, and unpriced station pathways. Route and Matrix share the corrected model in both directions and runtimes. See walking evidence and migration.
Add explicit arrival reserves alongside transfer reserves in both runtimes; retain actual timetable clocks and report the earlier planning deadline. See travel-time uncertainty for the supported contract and the separate historical-data calibration work needed for on-time probabilities.
Certify capped coordinate arrive-by bounds with a forward witness before accepting the faster search; retain the layered fallback and full journey tie rules. Reject malformed nonchronological native runs and correctly materialize selected continuity-bridge alightings.
Reduce repeated native allocation, endpoint projection, and journey-round work. Arrive-by retains matching reachability proofs through its exact deadline certification. See query performance for the mechanisms and reproducible public checks.
Use the same exact point-query certifiers in Rust and Node, including latest-departure certification for arrive-by. Share identical Matrix endpoints through search and journey construction, then restore every requested result cell.
Reuse validated prepared timetables during standalone startup, keep endpoint evidence in Rust structures, and share street-path source sweeps. Preserve full journey output by default; expose an explicit compact analytical Matrix format with the same timed witness.
Materialize the selected coordinate walking witness while its query token is current; retain exact street search for expired frontiers and transfers. Skip transit scans when either endpoint has no boarding/alighting access.
Begin the CLI tutorial and Rust quickstart with Boston: official MBTA/OSM inputs, a bounded street extract, date coverage, real places, arrive-by, matrices, and repeated queries.
Bound HTTP bodies, responses, queues, connections, and worker deadlines. Terminate stalled workers and verify successful replacement queries.
Preserve the selected station entrance/pathway when constructing standalone walking evidence. Retain uncertainty where source data cannot establish physical station access.
Preserve active GTFS departures for existing Reach branch edits by default, expose complete-network surfaces and optional street edges, and compare a direct walk within the point-query walking budget.
Verify extracted packages and configure platform/container checks. Local checks do not establish a completed cross-platform release or a hosted production deployment; deployments still require their own data, capacity, monitoring, and recovery validation.
API 1.0, City format 1, and Result schema 1 remain unchanged. See the changelog for subsequent changes.
VIGO 0.4.4
VIGO 0.4.4 corrects walking between coordinates on the same street segment and Reach boundaries. Rebuild Cities from raw inputs to apply the platform-area import correction and refresh prepared access and transfer data.
Route, Matrix, stop access, and stop transfers can follow a segment between two interior projections without an artificial detour through its endpoints. Both directions retain the walking budget and selected street attachment.
Transit Route and Matrix compare a direct walk by default. Very short trips can return walking; requireTransitRide: true still requires a boarding. Studio displays the returned travel mode.
Standalone Matrix returns timed walking legs when a direct walk wins, including requested geometry. Walking geometry retains both endpoints, including a zero-length walk.
Ordered Transit waypoints retain a boarding on each component leg across interfaces; a blocked CLI component keeps its failure status and diagnostics.
Reach retains partial street segments at time and walking limits, including interior origins that cannot reach either vertex. Street bundle v2 retains separate arrival intervals and clips displayed streets at the chosen cutoff.
surfaceSampling: "cell-center" evaluates destination coordinates on a fixed grid with the Matrix model. Street sampling remains the default and supports planned service changes. Walking-only Reach is available in both modes.
Reach forwards transfer caps and uses directed station egress, including its walking distance. Platform polygon outlines are excluded from the linear walking graph, and long street segments remain discoverable for coordinate attachment.
Studio adds Area calculation under Analyze settings and reads both v1 and v2 saved street results.
Departure-window recommendations break equal-arrival ties by fewer transfers, then less walking. Studio cards compare elapsed time from the requested departure, and itineraries identify vehicle changes with no scheduled time between them. The minimum transfer buffer remains selectable.
Native worker retirement retains the shared library used by process-wide thread pools. Updated Node bindings and repeated worker/process teardown checks cover resident kernels and typed-array cleanup.
GTFS transfer minima and station pathway times remain in force. A missing raster cell is not automatically an off-network destination; cutoff and walking constraints can also prevent a journey.
Reproducible checks use synthetic public fixtures: check:rust-routing-kernel, check:reach-engine, check:reach-scenarios, check:reach-grid, check:standalone, and check:cli. Packaged Studio checks verify the built application.
API 1.0, City format 1, and Result schema 1 remain unchanged. The default direct-walk comparison and street bundle v2 are the behavior and nested-output changes to review when upgrading.
The published VIGO 0.5.0 release at source c8da84fff7812d570fd5985cbcbcb83885587bf6 is the permanent prepared-data baseline. Future VIGO readers must continue to open these Cities without requiring users to rebuild them. Future standard City writers must continue producing data readable by that published runtime.
Keep the existing City manifest, routing/street SQLite layouts, prepared access and timetable snapshots, CCH members, and portable relative references compatible. Do not reuse a format identifier for an incompatible layout. Algorithm and performance improvements must retain the published representation. Additional optional metadata/indexes are allowed only when existing readers can ignore them and old Cities still work without them. A new required format or an automatic rebuild of valid 0.5 Cities violates this policy.
This applies to regular, wheelchair and scenario-collection data. It does not promise immutable GTFS/OSM contents or identical answers after a routing bug fix. Invalid/corrupt source data must still be rejected. Internal diagnostic traces are not a persistent data format. Public vigo.*.v1 results retain existing field meanings, units and time conventions; additions must remain optional.
Release gate
npm run check:format-050 builds candidate runtimes and tests both directions:
The candidate reader opens the frozen City produced by the published CLI.
The published Rust reader opens a City produced by the candidate writer.
Both execute transit, arrive-by, walking, matrix and reach queries. The test is also part of check:standalone, used by the engine and release CI matrices. The original archive-only CI build (run 37885649818) is preserved in the v0.5.0 release under *-baseline-c8da84fff781.tar.gz asset names. Downloads require no GitHub token and do not expire with Actions artifact retention. Every platform retains its original pinned SHA-256; the later, differently packaged 0.5.0 downloads are not interchangeable with these baseline assets. For offline checks, VIGO_050_REFERENCE_BINARY may point to an extracted package whose manifest, source identity and executable hash are verified.
The small synthetic GTFS/OSM and prepared archive live in test/fixtures/format-0.5.0. Never regenerate that archive, change its pinned hashes, or weaken its expectations to hide an incompatibility. It is a permanent historical reader test, not generated build output. Extend coverage with new fixtures while retaining this baseline. The initial fixture covers regular walking/transit; additional wheelchair, collection and large-network fixtures are still needed before claiming comprehensive format coverage.
The policy is permanent; the tests are evidence for the cases they execute, not a proof that all future code or every possible City is compatible.
Use the public CLI, HTTP, or Python interface to integrate VIGO. Studio's internal API and the native field dictionary serve different purposes and are not interchangeable with public application responses.
A passing public synthetic fixture verifies the exercised behavior. Use the exact build and target's test results when assessing an installation; an old test count is not a current release check.
Maintain the documentation
Markdown is the source for the single offline guide, docs/guide.html. docs/navigation.json lists its pages: each one renders a Markdown file, a section of one, or several sections composed in order, and aliases keep links to retired anchors working. Run npm run docs:build after changing a page, then npm run check:docs. npm run docs:standalone regenerates the OpenAPI document and native field reference from the Rust structs. The same guide is embedded by the engine at /, bundled by Studio, and shipped in engine archives; it works offline and includes print styles. There is no separately maintained PDF manuscript.
Research interfaces
These are bounded research tools with their own assumptions and evidence:
The validation archive retains dated development evidence. It is separate from the current usage instructions. Version history remains in the changelog.
Studio and the CLI share routing code; neither routes through the other. The public model is City → optional Scenario → Query → Result.
Studio renderer vigo command / Python wrapper
│ │
Electron preload → utility process CLI validation and output
│ │
└──────── shared City / query modules ─────┘
│
Rust timetable + street kernels
│
Result and diagnostics
Browser development uses loopback HTTP in place of the desktop memory channel, with the same handlers. The Studio HTTP API is internal; the stable programmatic boundary is the CLI.
Build and open
Build validates GTFS and OSM in a staging directory, creates timetable and street stores, prepares required native indexes, and publishes a complete City atomically. A failed build preserves the previous City. SQLite owns durable source and build data; production path search does not query SQLite at each routing step.
Store admission accepts current formats. Obsolete source stores require a rebuild; merge, compaction, and index preparation cannot upgrade them in place. Compaction happens before transfer topology and access preparation, and retires indexes tied to the previous database generation.
Open validates identity and loads the data a query needs. Active-service timetable snapshots and directed station-access contexts can be retained with the City. Derived caches can be regenerated from compiled data; missing required street indexes require restoration or a source rebuild. Ride geometry is loaded and aligned only for selected trips. See performance for cold, reopened, and resident timing boundaries.
New City builds persist the Rust drive hierarchy and both free-flow metrics. Drive startup maps these artifacts; it does not rebuild the hierarchy. The CLI prepares only the requested mode, and Matrix streams prepare each mode on its first use. Missing required drive artifacts cause a rebuild/restore error. Older Cities marked with an ephemeral drive hierarchy retain their existing behavior until rebuilt from source.
Computation ownership
In this guide, CCH means Customizable Contraction Hierarchies, the prepared street index used to accelerate path queries. CSA means Connection Scan Algorithm, which scans timetable connections in time order. A frontier keeps competing feasible options, such as earlier arrival versus less walking. A witness is the path or journey supporting a computed cost. Materialization assembles its stops, legs, and geometry into the returned Result. Admission validates whether input data can be used; prewarming prepares it before a query, and resident state stays in memory for reuse.
Rust reads, indexes, aligns, and clips selected GTFS shapes in one native cache. A reusable 8 KiB output buffer holds the sampled leg; JavaScript copies only its displayed points into itinerary objects. Both runtimes default to a 64 MiB shape-cache budget and at most 1,024 shapes. JavaScript handles store lifetime, orchestration, and public interfaces.
Operation
Implementation boundary
Transit Route
Native forward or reverse timetable search, directed street access, then selected-journey reconstruction
Walk / Drive Route
Native directed street search, CCH acceleration, and path geometry
Transit Matrix
Shared native forward scans per origin or reverse scans per destination; optional selected witnesses
Reach
Native timetable propagation and walking surface; JavaScript contours and Result shaping
Scenario
JavaScript validation and compilation of supported changes; native routing applies the compiled overlay
JavaScript owns request validation, identity resolution, orchestration, cancellation, and presentation. Rust owns active-timetable departure/transfer indexing, realtime timetable reconstruction, station path compilation, graph search, and timetable propagation; see the kernel implementation map. Source-feature omissions remain explicit in results; shared implementation alone does not prove correctness. Independent checks validate the declared model.
Module boundaries
Area
Ownership
src/server/gtfs/
Store schema, admission and identity; calendars, stop-access indexing and coordinate-access profile lifecycle; walking policy and plans; result assembly, realtime timetables, and network previews. The coordinator retains store lifecycle and native query orchestration.
src/server/runtime/route-worker-pool.mjs
Routing-worker leases, cancellation, prewarming, and memory pressure. The API entry point supplies the worker URL so source and packaged execution resolve the same worker.
src/map/
Network, journey, vehicle, and Reach feature builders; layer definitions, paint, and basemaps. VigoMap.tsx owns React effects, map events, and selection.
src/components/studio/
Sidebar, City library and source controls, and route surface. App.tsx owns application state, requests, and navigation.
These modules do not import their coordinating entry points. Public GTFS exports remain available through national-gtfs-store.mjs. Calendar caches still belong to each admitted store. Walking-anchor profiles remain private to the walking-plan module and are cleared when the coordinator invalidates their store. Realtime query symbols and weak timetable caches belong to the realtime module; decoded street-edge bundles retain one weak cache in the Reach feature module.
Stop projections use weak references to both the timetable and street record, retaining only the current access profile for each pair. Changing a profile replaces its projection; returning to an earlier profile recomputes it. Worker transfer failures reject the affected request and release the queue slot while preserving the worker for subsequent requests.
The remaining large files have tighter state coupling: GTFS import and query orchestration share store identity and cache lifetime; national-osm-store.mjs couples graph admission and native preparation; the Rust coordinate and timetable kernels share search workspaces and snapshot layouts. Split those along explicit state ownership boundaries, with routing, cancellation, and persistence checks, rather than moving arbitrary line ranges. Vendor code is maintained separately.
public/main.mjs owns windows, menus, dialogs, and the vigo://studio resource scheme. public/preload.cjs exposes a small desktop bridge to an isolated renderer without Node access. Engine runs in a utility process; packaged Studio has no TCP listener. Packaging copies the shared public/ distribution once.
Network shares one route/station selection across its tabs, map, and line diagram. Known feed namespaces resolve against the City timetable; ambiguous bare IDs cannot select an agency. Station scope includes platforms, while a platform keeps its own events. Briefings retain their original observation; background assessments do not move the map.
Surface
Owner
Trip times and patterns
AgencyTripTimetable, NetworkTimetable
Map and line
VigoMap, AgencyRouteLine
Station board and evidence
StopArrivalBoard, AgencyEvidence
Network briefing and retained evidence
AgencyPanel, AgencyBriefing, notebook
Route/vehicle timing
src/agency/routeOperations.mjs
The Network service owns observation polling, timetable inspection, briefings, and retained operational evidence. Its tool registry does not call Route, Reach, Matrix, online place search, or general web search. Those routing operations use the structured Studio controls and the CLI. Ask and City skill installation/execution are removed; existing notebook records remain readable without a schema migration. Optional model settings and evidence export belong to the service briefing.
Vehicle matching preserves service dates, including previous-day overnight trips. Network assessments share an immutable observation; method and denominators are documented in service assessment. Old ledger records remain readable, but new guidance requires current source identity. Notebook retrieval filters protect previously internal context in retained records.
Studio Reach retains packed reached-edge data, lazily reuses decoded coordinates across cutoffs, and serializes large map-source replacement. Changing a displayed cutoff does not require rebuilding the street graph. Runtime fixtures check map updates and rapid cutoff changes.
Storage and verification
The complete CLI City directory is portable; individual databases and native files are implementation details. Studio library settings, drafts, notebooks, and connections are separate application state. The operations ledger and replay remain active research interfaces with their own tests and storage. The LAMP reader can inspect existing City studies.
Use the contribution guide to choose checks. Routing fixtures verify feasibility and witnesses; desktop runtime fixtures verify interaction. Actual-provider evaluations are opt-in and separate from deterministic tests. No fixture or build proves field accuracy or model reliability.
The 0.4.2 architecture audit records the freeze review, remaining cost centers, and reasons for retaining compatibility code.
Itinerary geometry and identifiers
Rust reads selected shape rows directly from SQLite into numeric buffers and builds a distance prefix and a hierarchy of bounds in unit-sphere coordinates. The bounds skip sections that cannot contain a nearer candidate, including across the antimeridian and near the poles. The matcher still retains up to 16 points within 1 km per stop, evaluates the original haversine distances and tie rules, and aligns the complete trip monotonically to disambiguate loops. JavaScript samples the selected range from a compact coordinate column using the existing distinct-point stride and 512-point limit. Shapes with consecutive duplicates also carry an index of distinct source positions. Range clipping uses two binary searches and visits only sampled positions; shapes without duplicates use source positions directly. Short ranges use a single bounded pass. Final duplicate cleanup and distance calculation share one pass over the newly materialized coordinates. Prepared shapes remain subject to the store's entry and byte budgets, including the coordinate column, distinct-position index, spatial index, and bounded native candidate cache. A read-only source connection has a 2 MiB SQLite page-cache budget and closes when its routing store is disposed or invalidated. No itinerary answer cache is introduced.
Plan identifiers retain the existing 64-bit hash and Unicode code-point semantics. Rust performs that arithmetic; identifiers are selection keys, not security digests. npm run check:accuracy includes differential tests against the previous JavaScript shape matcher and identifier arithmetic.
Local OSM uses cartographic features retained in the City's SQLite street store. It does not query or load the pedestrian or driving accelerator. The /local-basemap endpoint returns roads and water in one GeoJSON collection. The former routing-edge renderer and its /local-streets endpoint have been removed.
Import and geometry
The importer first selects water multipolygon members, then includes their node references in the street import's existing selection and geometry passes. Untagged members are joined in either direction, and inner rings remain island holes. These temporary relation tables are dropped before publication. Map tables survive routing-store compaction and travel with the SQLite artifact; no external source file or sidecar is required at runtime.
Only motorway, trunk, primary, secondary, and tertiary roads are drawn. Links enter at zoom 12; lesser road classes never enter the basemap. The routing graph still retains its original walk/drive eligibility rules. Rivers, canals, water polygons, and directed coastlines are independent of routing permissions.
Geometry is rounded to six decimal places and simplified at three detail levels during import. Identical geometries share one stored row across levels; invisible levels are omitted. Line pieces contain at most 128 input vertices, and persisted geometry contains at most 8,000 vertices per feature. Water relations exceeding 200,000 input vertices, containing unsupported nested geometric members, or missing complete rings are skipped and counted in metadata. Oversized source ways are also skipped for display. Display geometry is never used for routing.
Coastal water follows OSM's land-left, sea-right direction. The query clips and joins shorelines, closes complete chains along the viewport boundary, and retains coastal islands as holes. A nearby shoreline supplies the side for views wholly on land or sea. Interior breaks suppress ocean fill rather than inventing a coast. The PBF header bounds, or the retained feature extent when the header omits bounds, limit coverage. This does not reconstruct missing ocean geography beyond a regional extract.
Runtime bounds
An SQLite R-tree selects the requested area, detail level, feature class, and zoom before loading geometry. Coastline searches do not scan unrelated roads.
Each request opens a read-only connection with a 4 MiB page-cache target and no memory mapping; it closes the connection after querying. This is a SQLite cache setting, not a total process-memory guarantee.
Responses contain at most 4,500 features and 80,000 vertices. Water has a separate half-budget so it cannot displace all roads. SQL sorts feature metadata and fetches geometry only after it fits the response budget.
Coastline assembly separately caps both candidate input and clipped geometry at 4,096 pieces and 80,000 vertices. Boundary joins use sorted crossings instead of scanning all crossings repeatedly. Island assignment has a containment-work budget; exceeding either geometry or work limits keeps bounded shoreline outlines and omits ocean fill.
The browser requests 18% padding around its viewport, reuses it for small pans, debounces moves, aborts superseded fetches, and discards geometry on City or basemap changes. A fresh OSM import invalidates the loaded geometry.
Worker updates are serialized with only the newest waiting viewport retained. City changes hide the old layers immediately and wait for active worker work before removing or reusing the source, preventing delayed geometry from the previous City from reappearing.
One GeoJSON source serves all five map layers. Its tile index stops at zoom 14 with a 32-pixel buffer and 0.75 simplification tolerance. Light/dark changes update paint without fetching geometry.
Responses report sampling and incomplete geometry. There is no city-wide JavaScript geometry cache or tile-provider fallback.
These limits bound retained feature data, not total Electron, worker, tile-cache, or GPU memory. Viewport queries run synchronously; an R-tree query over many matching features can still take longer than a small-city query. Full-region import and prolonged large-city use require separate memory and responsiveness measurements.
Existing street stores remain usable for routing. A store without localBasemap metadata returns needs-import with a fresh-import instruction; it cannot recover road classes or water from routing snapshots that omitted them.
Verification
Run npm run check:map, npm run check:osm-pbf, node test/check-city-build-equivalence.mjs, and node test/check-map-runtime.mjs. The basemap fixture checks road selection, lake holes, coastal islands, offshore and inland views, query budgets, pathological coastline clipping, R-tree access, compaction, and independence from routing snapshots. The Electron fixture renders both appearances through the local Studio protocol with all external HTTP requests blocked, overlaps updates with repeated City-source replacement, verifies that only the newest geometry renders, and checks source removal.
For manual visual review, the runtime fixture accepts VIGO_BASEMAP_PREVIEW_FILE pointing to a local GeoJSON response and VIGO_BASEMAP_SCREENSHOT_DIR for light/dark screenshots. Keep downloaded extracts and generated images outside tracked source files.
Measure the operation the caller waits for. Keep source data, City revision, query semantics, output detail, and status counts fixed when comparing runs.
For the shared native timetable workspace and a reproducible component comparison, see Timetable query performance.
Operation
Start
End
Build from raw files
Invoke vigo build with local inputs and a new output City
The complete City is published and the command returns
Raw files to first answer
The same Build invocation
The first complete Query Result returns
Reopen a prepared City
Start a new runtime on a complete saved City
Its first Query Result returns
Resident query
Submit to an already open City
The complete Result returns to the caller
Report downloads and runtime installation separately. State whether process startup, transport, export, and operating-system file-cache effects are included. A new process does not imply a cold filesystem cache; reading a saved Result is not a new computation.
What the timers include
network.json.timing describes compiler stages. totalMs begins inside the compiler after input/staging checks and ends before writing the manifest. It excludes process startup, shutdown, final City publication, and the first query. Stages may overlap; their durations cannot be summed into wall time. osmBuildMs can include waiting until the parent collects the worker result.
Route searchStats.queryMs measures the timed route-search and selected-journey work, including street access. It excludes fare annotation performed by the caller, final CLI decoration, transport and Result serialization. CLI routeMs / computeMs wraps routing, fare annotation and CLI decoration; it still excludes final serialization and transport.
materializationMs covers selected itinerary assembly. Its components include trip connection loading, metadata lookup, shape extraction, access/egress geometry, leg normalization and identity construction. Components need not sum to the enclosing timer. A geometry improvement is not automatically the same improvement in the complete response.
engineQueryMs measures native timetable work. For arrive-by, this includes reverse feasibility and forward selection; arriveByNativeQueryMs and forwardEngineQueryMs identify those components. Street access and geometry are outside that timetable total.
Matrix computeMs covers engine execution; requestPreparationMs and resultAssemblyMs describe caller preparation and row assembly. openMs reports mode initialization, including the first request of each mode in a stream. JSON serialization and transport require an external timer. Optional journey timings describe witness rendering, not independent searches or a share of batch time.
Use an external elapsed timer for complete Build and query latency. A small native search time does not establish the same user-perceived response time.
Fare parsing and localized display have separate costs. Whole-unit prices within the exact-integer fast path need no currency formatter; fractional and large prices still consult currency precision, and displayed labels still format currency symbols. Include the first fare-bearing result when measuring a fresh process. A preceding blocked route does not initialize every successful-route presentation path.
Preparation and reuse
Build includes import, required street indexes, stop transfers, station access, and publication. The first query may also compile its active-service timetable and align selected ride geometry. Report this cache-miss preparation separately from resident query time.
Portable binary timetable snapshots and prepared station access can be reused after moving the complete City. Timetable diagnostics distinguish loaded from written; a loaded snapshot reports compileMs: 0. New service patterns, changed walking policies, evicted caches, or older derived formats can require preparation. Missing required street indexes are errors, not permission to use a different graph.
Queries are recomputed. disableCache: true on transit Route/Matrix disables access/path caches while retaining the prepared City. Larger walking matrices may build temporary destination indexes; that setup belongs in the measured request.
Node Reach retains up to 64 next-departure schedule diagnostics per opened source store and active service context. This lookup preserves raw SQLite times and the inclusive 24-hour horizon; changing the service context clears it, and replacing the source creates a new lookup. It is immutable schedule metadata and remains available with disableCache: true. Reach still recomputes the timetable frontier and pedestrian surface. Measure first or changed departure clocks separately from repeated clocks; diagnostics.timetable.departureLookupMs exposes this cost alongside residentPrepareMs, search and surface timing. A faster repeated diagnostic lookup does not establish a faster native search or City opening.
Native timetable preparation
Standalone profile/trace output includes timing.timetableActivationMs, the time spent activating the service timetable during that request. A source preparation also reports timing.timetablePreparation with setupMs, sourceAndRealtimeMs, indexMs, and kernelMs. Setup includes calendar resolution and retiring the preceding timetable; source includes SQLite loading and realtime application. These scopes belong to execution and exclude public formatting, serialization, transport, and initial City opening. A reused timetable has no new preparation record. Profile scopes can overlap total execution; do not add them to it.
Since 0.4.1, Rust constructs per-stop departure order, deduplicates transfers, expands station fallback links, and packs transfer adjacency. JavaScript resolves source IDs and passes typed arrays to the native preparation operator. This runs on active-timetable cache misses; loading a compatible persisted snapshot bypasses it.
Measure input marshalling and native preparation together when comparing the migration with the previous implementation. Keep SQLite reads, native search-index construction, snapshot loading, and resident query time separate. Preserved transfer ordering and snapshot layout do not by themselves establish a speedup. Filesystem lifecycle, source admission, and Result presentation remain JavaScript responsibilities.
Compare equivalent work
Record exact date/time, coordinates or stop IDs, walking speed and limits, boarding requirement, transfer cap, horizon, realtime snapshot, and output detail. Allowing a walk-only answer changes the workload. Separate ready, blocked, and error counts; a faster blocked result or a different journey is not an equivalent successful query.
For Build comparisons, hold raw inputs and compiler options fixed and compare compiled content; fresh revision IDs and build timestamps are expected. Performance checks complement accuracy checks, not replace them.
Cold service and street preparation
The native service reader opens the admitted SQLite source read-only and packs active connections directly into column buffers. JavaScript retains source admission and memory guards; Rust retains connection permissions, trip ordering, and discontinuity rules. A missing native reader is an error, not a JavaScript fallback. VIGO_ACTIVE_KERNEL_PERSIST=0 disables both reading and writing the active timetable snapshot when measuring compilation from source.
Prepared street graphs and CCH structures are validated concurrently during native construction. Validation still scans every required array. Path-query scratch distances use zero-filled storage with an encoded unreachable value, so the first path does not need to fill city-wide distance and predecessor arrays. These changes do not reuse previous route results.
Since 0.4.2, both persisted and in-memory Drive hierarchy construction use one 33-percent balanced four-axis flow cut per component. New City builds persist the hierarchy; opening that City loads it instead of repeating construction. Both retain every node and edge and use the same exact CCH search and distance certification; ordering can change the chosen witness between equal-cost paths. Arc ordering uses a shared stable counting sort. Native kernel diagnostics expose the hierarchy's arc count so preparation time can be evaluated alongside its size.
For a cold comparison, alternate baseline and candidate in separate fresh processes, include imports, graph opening, timetable compilation and result serialization, and compare route contents as well as latency. Keep immutable City inputs and hierarchy preparation policy identical. In particular, an in-memory drive hierarchy must be rebuilt on both sides; loading a saved hierarchy only on the candidate is not a cold code speedup. Report operating system page-cache control separately from application cache isolation.
Resident routing and realtime updates
For warm routing, retain the prepared City but disable access and path result caches. Verify the per-request cache diagnostics, rotate endpoints and departure times, and report native search separately from geometry materialization and whole-request time. Shared immutable indexes are preparation, not query answers.
Realtime measurements need two workloads: queries against an already prepared snapshot, and changed snapshots that force reconstruction and native indexing. Keep freshness, service date, applied update counts and fallback status in the retained evidence. Controlled updates on a real timetable measure computation; they do not establish live prediction accuracy. Compare reconstructed columns and route results, including cancellation and skipped-stop behavior.
Drive constrained-search buffers and traffic weights are allocated on demand. Report reserved bytes separately from resident memory: operating systems can back zero-filled allocations lazily, and allocator retention can obscure RSS changes after buffers are released. A normal CCH query should not allocate the constrained-search workspace merely to keep a kernel resident.
Compact street indexes and geometry
The v2 hierarchy stores seven aligned query columns. Its size without customization data is 104 + 16 * nodes + 12 * hierarchy_arcs bytes. Walking indexes use exactly this layout. Driving appends gzip-compressed input mappings for traffic updates; normal queries map the query columns without decoding the mappings. Inverse rank and arc tails are reconstructed only when customization needs them. Serialization uses a fixed 16 KiB buffer.
Transit shape coordinates and their distinct-point index remain native. Each selected Studio leg writes at most 512 points into an 8 KiB worker-local scratch buffer, then copies those points into its owned response. No full shape is exported or retained in JavaScript. The exhaustive clipping checks include loops, duplicate runs, signed zero, endpoint preservation, aliased endpoint views, and repeated buffer reuse. Shared output buffers are rejected at the native boundary.
Measure shape-cache bytes, temporary allocation, and whole-request latency separately. Reducing duplicate coordinates need not reduce timetable search time, and a hierarchy file reduction is not the same percentage reduction in the complete City.
The standalone Rust executable and Node interface use the same timetable kernel. Standalone transit Route requests obtain a journey through the shared Matrix kernel with one origin and one destination. Matrices can request scalar times or full journeys.
Journey construction allocates its scratch workspace lazily and retains it with the immutable timetable. Each source clears only the frontiers it touched; each boarding round visits only reached stops. Run generation tags separate rounds, sources, and requests, including wraparound. The workspace contains no cached answers. Changing direction, endpoint policy, or boarding limits still performs a fresh search. Replacing the timetable also replaces its scratch workspace.
Scalar Matrix queries prepare the common endpoint arrays once per batch and reuse the changing source or destination buffers. Validation and routing objectives are unchanged. Depart-at still shares a forward search per origin; arrive-by shares a reverse search per destination. Journey selection keeps the same dominance rules, run order, transfer rules, time horizon, and tie breaks.
For coordinate batches, journey rounds finish after every reachable target has attained its own exact scalar time bound. The final round completes before stopping, preserving boarding and walking ties. Targets without a route retain their blocked result. Selected-stop requests that permit terminal transfers keep the general round path. Endpoint projection also shares identical directed access searches within one batch, even when persistent endpoint caches are disabled; stop IDs remain part of the standalone endpoint identity. Each input still receives its own output row or column.
Arrive-by Route retains the scalar forward reachability envelope through successive capped and deadline certifications. Reuse requires identical origin and destination seeds, costs, departure, horizon, and terminal-transfer policies. Extending a deadline admits the remaining timetable events; contracting it keeps a conservative superset that exact rounds filter against the requested deadline. A new scalar search or a separately built forward envelope invalidates the retained identity. The reverse latest-departure search and exact deadline/boarding/walking certification still run.
Capped coordinate arrive-by searches first obtain the unrestricted latest departure and seek a forward witness at that same boundary. They accept the bound only when the witness reaches the deadline within the boarding cap; an unrestricted blocked result also proves the capped result blocked. Otherwise the existing layered reverse scan resolves the cap. A 1×1 Matrix uses this same certified bound before its unchanged journey rounds. Work counters include the proof and any fallback; no trip choice or tie criterion is removed.
Retaining scratch trades some resident memory for fewer allocations. Native workspaceBytes diagnostics include the journey arrays and their retained capacities. Scalar-only users do not allocate journey scratch.
The scalar point scan rejects strictly later arrivals before computing boarding and walking tie-breakers; equal arrivals still receive the full comparison. An epoch-scoped lower bound on stop arrival skips boarding searches when every label is in the future. It uses eight additional workspace bytes per stop and does not change prepared data. The preliminary unrestricted scan for a capped point query retains labels and its forward run envelope but omits predecessor writes and intermediate chain construction. Exact capped rounds still recover the final journey with the existing boarding and walking objectives.
For ordinary Node Route responses, the adapter passes only the plan diagnostic fields consumed by the shared Rust public formatter: realtime metadata, arrival reserves, and the service clock date. This avoids serializing and parsing unused search trees. Full journeys, alternatives, and geometry remain present; requested summary, profile, and trace responses retain their existing diagnostic input.
Coordinate routes and materialization
For capped fastest depart-at queries, the Node adapter permits the fused Rust call to return the selected access and egress witnesses, including walking geometry. The existing native guard uses this compact result only for certified one- or two-boarding routes. Uncapped queries, balanced preferences, departure-window alternatives, and arrive-by deadline certification retain the full candidate frontier. The change removes candidate conversion and subsequent path calls without limiting the search.
The standalone adapter retains immutable route metadata and trip-to-shape identifiers within each opened City. Journeys still reconstruct their active timetable stop sequence and geometry on every request. The metadata maps are bounded by the City's route/trip counts, are released with that City, and remain separate from endpoint or answer caches.
When opening an access context, the standalone stop projection parses only the normalized fields it consumes and validates JSON in skipped fields. Coordinate type checks and the null-coordinate exception for location types 3/4 remain unchanged. This changes reader allocations, not the portable City layout.
Reached-street propagation uses a monotone queue keyed by exact finite nonnegative floating-point duration bits. Equal-time entries use the existing walking-distance, node, seed, and witness ordering. A nonmonotone insertion falls back to the original heap. This does not round times, change walking budgets, or alter Pareto dominance. Node and standalone use the same operator. Compare full rasters and edge evidence, alongside measured execution and process memory, when evaluating queue changes.
Compiled GTFS shapes and their geometric alignment candidates are retained separately in a City-owned least-recently-used store, limited to 4,096 shapes and 256 MiB of estimated geometry storage, matching the Node interface's shape-cache limits. Lookups examine the most recently used entries first. This avoids the much smaller standalone cache repeatedly evicting geometry on varied city-wide workloads; the larger upper bound can increase resident memory. Sizes are accounted again after alignment grows candidate data. Oversized shapes are used for the current request and released. Timetable paths, realtime stop sequences, and selected shape slices are still reconstructed for each journey; the store does not retain route answers.
The standalone store also retains up to eight exact stop-to-shape alignments per shape within the same byte budget. The complete active stop coordinates, including their floating-point bits, form the key. A changed realtime stop sequence cannot inherit the scheduled alignment. Materialization copies only the selected shape slice. Point endpoint roles use the shared two-worker native executor. Batch endpoint evidence remains typed and is shared for identical request endpoints; it becomes JSON only when a response includes that evidence.
Generic street geometry keeps the original directed snap pairs, raw path distance, and tie rules. CCH path reconstruction shares one forward sweep across the candidate destinations for a source. Every destination still performs its exact backward sweep and shortcut reconstruction. The shared state is scoped to that batch call, so a subsequent source or metric never inherits it.
Standalone startup and output boundaries
The standalone City reader deserializes the used access-context fields directly into native structures and skips unused projections. Scheduled transit can load the compiler's portable active-service snapshot after checking database identity, access policy, active services, transfer projection, dictionaries, canonical array layout, and native kernel invariants. Missing or invalid optional sidecars fall back to the existing SQLite preparation path. Realtime and modified street-transfer policies use source preparation. timing.timetableSource exposes the selected preparation path; timing.totalMs still excludes process startup and initial City opening.
Rust Matrix defaults to full display and walking-evidence detail. Node Matrix returns compact timed witnesses. For a comparable analytical workload, request Rust journeyFormat: "compact" with includeJourneys: true, or request duration-only matrices from both. Compact changes response detail only: the same native journey search runs, with the same trips, boarding sequences, clocks, and transfer count. Full remains the default and the required format for geometry and walking-evidence consumers. The stream uses bounded 64 KiB serialization scratch instead of retaining another complete encoded response.
Reproduce a kernel comparison
Build each version with npm run build:rust-routing-kernel using the same toolchain and release settings. Save the earlier native/vigo-routing-kernel/vigo-routing-kernel.node under a separate path before building the changed version. Then run:
The script generates deterministic synthetic timetables with 256 and 4,096 served stops. It measures 1×1, 16×16, 1×128, and 128×1 requests in both time directions, with and without journeys and terminal transfers, using a three-boarding cap. Baseline and changed calls alternate execution order. Every call must preserve times, blocked cells, and full journey contents. Reports retain search counters separately because an optimization can reduce the work. They also include individual samples, medians, p95, native timing, and resident workspace bytes.
The outer timing includes Node-API argument/result conversion. Native timing excludes that conversion. Both exclude City loading, endpoint street routing, geometry, fares, JSON serialization, HTTP, and process startup. No answer cache is used; the prepared timetable and scratch memory remain resident. The first request for a workload is measured on the existing kernel and is not a cold-process or cold-filesystem measurement.
Quality checks
test/check-timetable-query-reuse.mjs compares a reused kernel with fresh kernels across changing endpoints, time directions, boarding limits, endpoint transfer policies, empty access, and invalid requests. Rust unit tests exercise generation wraparound and recovery after a journey-bound error. Run the existing native matrix, independent routing-accuracy, transfer, equal-time, and standalone parity checks as well: agreement between two adapters does not independently prove the shared kernel correct.
For workload measurements, use complete caller-visible Route and Matrix requests on representative prepared Cities. Alternate identical requests between resident baseline and candidate processes, exclude a stated warm-up round, retain all responses, and compare complete route semantics. Keep cold loading, street access, timetable search, journey/geometry construction, serialization, and transport separate. The standalone engine already moves request execution into Rust; additional migration should target a measured remaining cost and preserve each interface's supported behavior. Private City inputs and workload evidence belong in the private benchmark repository.
Standalone query execution
Capped coordinate point queries use the same destination-directed exact certifier as Node Route. Arrive-by first proves the latest departure, then certifies boardings, walking, and actual arrival within the deadline. Unsupported cases retain shared Matrix reconstruction; selected-stop and unbounded queries retain that path. Full geometry and walking evidence remain part of the response. Equal objective values can select different source-valid transfer stops; check source legs and objectives separately from byte equality.
Transit matrices group repeated endpoint identities before timetable search and journey construction, then restore every requested row and column. Coordinates and stop identity must both agree; different co-located stops cannot share permissions. Full and compact output remain explicit choices, and scalar requests retain their original dimensions.
VIGO uses one exact Transit Matrix search for every City and OD set. City names, endpoint distance, and matrix dimensions do not select different algorithms. Independent reference implementations live in tests and never supply production answers.
Accuracy has two parts: the search must solve the declared model, and the model must faithfully represent the input data and the intended trip. A fast result or agreement between two VIGO interfaces establishes neither part by itself.
Reproducible independent checks
Shell
npm run build:rust-routing-kernel
npm run check:accuracy
# Extend the same checks to 100 deterministic generated networks:
VIGO_ACCURACY_SEEDS=100 npm run check:accuracy
test/check-routing-accuracy.mjs generates its own public synthetic inputs. A failing run retains the input directory and reports its seed and query. It uses:
Floyd–Warshall, implemented independently from VIGO's routing and contraction code, to check every directed walking distance and driving travel time on weighted graphs. Alternating graphs allow only forward edges, so unreachable OD pairs are checked as well. Drive witnesses are checked edge by edge against the input weights.
Whole-trip enumeration from generated raw GTFS, independently of VIGO's compiled connection scan, to check earliest arrival and latest departure. The fixtures exercise pickup/drop-off restrictions, staying aboard restricted stops, dwell, midnight, inactive dates, missed connections, published same-stop minimums and forbidden transfers. Zero-duration trips are deliberately placed in reverse dependency order; all scans resolve same-time connections to a fixed point.
Every stop-to-stop OD at each boarding-time boundary on the generated transit networks. This covers each distinct exact-stop departure interval in those fixtures. Separate queries check Matrix permutations, duplicate endpoints, scalar Route parity and materialized ride times and permissions.
Separate raw GTFS fixtures verify 0-, 30-, 90- and 300-second platform transfer rules, parent-station expansion, and forbidden transfers through Route, Matrix and arrive-by queries.
The normal suite runs eight seeds on every supported CI operating system. Larger runs use the same algorithms and assertions. They do not select easier queries based on a City's identity or on previous outcomes.
Declare the model before comparing answers
The engine adds no implicit boarding buffer. Same-stop vehicle changes honor published GTFS minimum transfer times and forbidden transfers; staying aboard does not incur a transfer minimum. The minimum affects boarding readiness, not the alighting time or final egress. Explicit transfer edges retain their durations without an added boarding margin or a 60-second floor. Native diagnostics report transferBoardSlackSeconds: 0. A published platform-to-platform transfer rule takes precedence over the station walking fallback.
GTFS service dates, after-midnight times, pickup/drop-off permissions and transfer rules must come from the same input snapshot. Their definitions are in the GTFS Schedule Reference.
Record the exact date, time zone, departure/arrival objective, walking speed, per-endpoint walking limit, direct-walk limit, transfer policy, search horizon and live-state snapshot. Distinguish exact selected stops from arbitrary coordinates. Coordinate snapping and street access are part of the query model and require their own verification.
Checks required for a real City
Validate compilation. Check raw GTFS references, calendars, exceptions, permissions and transferred identities against the compiled City. Inspect omitted or unsupported features. Check directed OSM access and connectivity, including isolated components, bridges and one-way streets.
Verify journey witnesses. Each ride must reference an active raw trip, follow its stop order, use legal boarding/alighting events and preserve published times. Every street segment must follow allowed directed edges. Check endpoint connectors separately so a correct graph path cannot conceal an invalid snap.
Compare independently. Run the same inputs and query policies through an independent router such as OpenTripPlanner. Its route-request documentation describes configurable routing policies. Retain both itineraries and investigate each disagreement; matching durations alone is insufficient, and another engine's answer is not ground truth.
Test changes that should preserve or constrain the answer. Rename IDs, reorder input rows and Matrix endpoints, move the complete City directory, and repeat after unrelated queries. Answers should remain stable. Under identical horizons and policies, removing service must not improve earliest arrival, and adding service must not make it worse.
Retain failures. Keep false reachable results, false blocked results, invalid witnesses, timing discrepancies, setup failures and query errors as separate outcomes. Save the smallest reproducing input and add a regression before accepting a fix. Do not change the oracle to match an unexplained engine answer.
For exact graph and timetable comparisons, require zero feasibility disagreements. Use only the documented public timestamp rounding tolerance when comparing numeric results. Equivalent optimal itineraries may differ in geometry or route choice; validate feasibility and the objective before comparing presentation details.
What passing means
The independent suite validates the generated graph and timetable models under their declared policy. It does not establish correct coordinate snapping on arbitrary OSM extracts, complete GTFS transfer-rule support, live-provider correctness, or real delivered travel times. Those require retained City-specific input and witness checks. A universal algorithmic guarantee comes from the algorithm's invariants and input assumptions; finite testing supports it but cannot prove every possible City or OD.
The following admission limits and recovery behavior apply to VIGO 0.5.2. See public results for the current result schemas.
Memory and request admission
The runtime uses the smaller of host RAM and Node's reported container limit to choose its default budgets. Configuration accepts finite positive integers; invalid values use the default, and valid values are clamped to the supported range. Zero no longer disables the timetable byte guard.
Timetable admission counts the active service slice before allocating native arrays, estimates its size, and checks retained bytes after construction. Realtime reconstruction estimates the resident scheduled view, rebuilt arrays and replacement calls before native compilation against the same budget. Default cache budgets now scale with capacity instead of imposing large minimums. This adds a count query to cold timetable construction. No latency improvement or large-City performance result is claimed.
The pool rejects excess work with status 503. Capacity waits have a deadline, and a worker watchdog requests termination when an operation exceeds its deadline. Under memory pressure the pool avoids adding a second City until an unleased idle worker can be retired. Worker diagnostics expose limits and pending counts. Shutdown wakes waiting requests and permanently closes the pool.
Within queued query work, Studio prioritizes point Route, departure-window, and street Route requests. An older analytical job runs after at most four point-query bypasses. Preparation and unknown operations preserve their queue order, and running native work is not preempted. Worker diagnostics expose maximumPointBypasses.
These are admission and retention safeguards, not a hard whole-process memory limit. V8's worker heap limit excludes native arrays and mapped street data. Imports, realtime reconstruction, result assembly, and serialization can still have temporary allocation peaks. The RSS check cannot interrupt a native allocation already in progress. A native call must return before thread termination can complete; operating-system memory exhaustion remains possible. Street and timetable budgets must be considered together for large Cities.
Recovery
A routing-thread failure rejects its active and queued work, waits for termination, and reopens authoritative City data for a subsequent request. Structured-clone failures reject only the affected job and leave the worker usable. No failed query is converted into a fabricated journey.
Studio keeps its window when the Engine exits. It allows three starts within a rolling minute, including the initial start, with increasing restart delays. Startup has a 30-second deadline. Once the restart budget is exhausted, a later request can try again after the rolling window permits it. Engine startup failure does not prevent the window from opening. Pending requests fail rather than being replayed automatically, because some requests change stored data. Studio admits at most 128 Engine requests, including those waiting for startup. When the Engine is unavailable or the restart budget is exhausted, desktop API requests return a structured 503 response. The interface can show the failure without waiting for an unresolved protocol request.
Packaged Engine environment filtering lives in public/engine-environment.mjs. Studio supplies its own application-data path after filtering so startup and recovery use the same configuration directory.
Smaller tests with real execution
test/check-national-runtime-isolation.mjs builds actual GTFS and OSM inputs, uses the production worker and native kernel, verifies exact journeys after thread termination, rejects a 50,000-trip input under a 4 MiB budget, verifies subsequent small-City routing, and checks overload and shutdown. It also imports a 5,000-trip timetable that fits the scheduled budget, rejects a realtime reconstruction above that budget, and verifies scheduled routing still works. It replaces the simulated worker and several overlapping lifecycle suites.
test/check-engine-recovery-runtime.mjs runs the actual Electron entry point, kills Engine processes, checks health after each restart, checks the retained window and sandbox settings, and verifies the crash limit and subsequent 503 response. Browser polling uses real timers. Streaming and provider-deadline checks use real local sockets. Operations permissions, approval invalidation, SQLite transactions, audit history, and reopening retained records run against the production operations service.
Canned model replies, substituted providers, worker emulators, global clock patches, browser-method spies, and injected storage exceptions were removed. Routing and vehicle-frame calculations accept explicit observation instants; other browser checks use current timestamps and real timers. Small generated GTFS/OSM and observation inputs remain test data, and production algorithms compute their results.
Pruning removes coverage previously supplied by canned model conversations, specific renderer refresh spies, simulated DNS changes, native-binding substitutions, forced file-deletion and storage-quota errors, and server-wide clock replacement. Those assertions are not counted as equivalent real integration coverage. The retained UI suites cover rendering, source updates, navigation, selection, draft persistence, and damaged-storage handling. Actual model behavior requires separately configured live evaluation; the release suite makes no model-quality claim. Frozen replay inputs and saved-study readers remain because runtime and research consumers still use them.
Verification on this checkout
The dated 0.4.2 verification record is retained in the release audit. Checks cover native routing comparisons, TypeScript, import/request security, CLI/City portability, operational calculations, actual worker/Engine recovery, real browser interactions and the macOS ARM64 package. They do not establish other-platform behavior, live provider quality, or whole-process immunity to memory exhaustion.
This matrix is the public semantic boundary for VIGO routing. “Imported” alone is not support: the routing column states whether a field changes path feasibility. New raw imports record a feature inventory. Fixed-stop pickup and drop-off permissions are compiled per connection: illegal board points are absent from the departure index, illegal alight points cannot create a reachable state, and a passenger already aboard may remain on the trip through either kind of restricted stop. Unsupported transfer semantics are omitted from the generic stop-pair transfer graph. The remaining network is routed as a supported_scheduled_core, with any exclusions attached to every result. This is qualified coverage, not an exact claim over the whole feed. Among otherwise importable GTFS semantics, only multiple agency timezones currently block the whole store.
GTFS feature or case
Import
Routing behavior
Classification
calendar.txt only
Yes
Weekly service within start/end dates.
Supported.
calendar_dates.txt only
Yes
Type 1 adds and type 2 removes service on the exact date.
Supported.
Calendar plus exceptions
Yes
Exceptions override the weekly calendar for the date.
Supported.
Missing both calendar tables
Rejected
No service date can be established.
Rejected malformed input.
Times greater than 24 hours
Yes
Preserved as seconds on the originating service day.
Supported within the query horizon.
Trips crossing midnight
Yes
Connections remain ordered on the originating service-day coordinate. The caller must query that date with a value above 24 hours; VIGO does not automatically merge yesterday's active trips into a 01:00 civil-date query.
Supported only under explicit service-day coordinates and within the horizon.
Trips extending across more than one midnight
Parsed
Same coordinate model; default horizon may exclude them and the public CLI clock is bounded.
Bounded; caller must set a sufficient horizon and service-day coordinate.
Invalid minute or second fields
Rejected
Values such as 08:60:00 or 08:00:60 do not normalize silently.
Rejected malformed input.
Agency timezone, one timezone
Inventoried
Caller supplies local service date and clock; no zoned-instant conversion.
Partial; DST instant semantics unsupported.
Multiple agency timezones in one store
Inventoried
Exact routing is blocked.
Unsupported and visible.
frequencies.txt, exact_times=1
Yes, bounded
The template trip is expanded into deterministic fixed departures for each declared frequency window. Generated instances retain route, service, permissions, and shape identity.
Supported for bounded deterministic windows.
frequencies.txt, exact_times=0
Retained for inspection
No stochastic or deterministic headway waiting model is inferred. The template trip is omitted from compiled connections.
Excluded from the supported scheduled core; visible limitation.
Overlapping frequency windows
Yes, bounded
Deterministic windows expand independently and duplicate generated departures collapse by trip/departure identity; inexact headway templates remain excluded.
Supported for bounded exact_times=1; visible limitation only for exact_times=0.
Shapes present
Yes
Ordered trip stops are matched to a monotone shape slice.
Supported for rendering; feasibility never depends on shape.
Shapes missing or unusable
Yes
Timetable route remains valid. Ordinary network lenses show no inferred route alignment; selected-itinerary and Shape/Risk QA views may show a dashed, explicitly inferred stop sequence.
Timetable supported; published ride geometry unavailable.
Duplicate stop names
Yes
Identity is stop_id, not name.
Supported.
Parent stations and platforms
Yes
Explicit parent transfer rules expand to child service platforms; selected stations expand to service members. Without an explicit rule, service members share a modeled 120-second connection, reported as parent_station_fallback with schematic geometry.
Supported for location_type 0/1; fallback time is not a measured station pathway.
Deeper location_type hierarchy
Parsed as stops
No complete entrance/boarding-area hierarchy model.
Partial.
pathways.txt direction and traversal time
Yes
Creates directed transfer edges; explicit traversal times take precedence. Stairs with a stair count and fare/exit gates receive labeled estimated costs when time is omitted. Bidirectional rows create the reverse edge. The full hierarchy is not preserved.
Partial.
Pathway wheelchair/slope/width attributes
Profile dependent
Applied during build --wheelchair; ordinary Cities inventory them without filtering.
Directed recommended transfer using declared/default duration.
Supported within transfer model.
Transfer type 1 timed transfer
Inventoried
The guarantee is not represented; the row is omitted from the generic stop-pair transfer graph. Other supported scheduled service remains routable.
Excluded from the supported scheduled core; visible limitation.
Transfer type 2 minimum time
Yes
Declared minimum traversal time is enforced.
Supported.
Transfer type 3 forbidden
Yes
Directed pair, including expanded parent/platform pairs, is forbidden and cannot be regenerated by proximity. A same-stop prohibition blocks reboarding after a ride while preserving an origin's initial boarding.
Supported with permanent SQLite, compact-kernel, and matrix regressions.
Transfer type 4/5 in-seat semantics
Inventoried
Linked-trip continuation is not represented; the row is omitted from the generic stop-pair transfer graph.
Excluded from the supported scheduled core; visible limitation.
Route- or trip-scoped transfer selectors
Inventoried
The stop-pair schema cannot preserve the selector, so the entire scoped row is omitted rather than generalized to every trip.
Excluded from the supported scheduled core; visible limitation.
Nearby stops without explicit transfer
Prepared
One native multi-target street search creates every directed OSM-reachable alight-to-board edge within 500 m. Each edge stores its routed distance; unproved radial pairs remain visible counts.
Supported within the City street model.
pickup_type=1 / drop_off_type=1
Yes
The complete scheduled trip is retained. Boarding is prohibited only at a stop with pickup_type=1; alighting is prohibited only at a stop with drop_off_type=1; through-riding remains legal.
Supported for fixed-stop routing.
pickup_type / drop_off_type values 2/3
Inventoried
The complete scheduled trip is retained, but rider-agency coordination is not a request capability, so the affected board or alight event is conservatively unavailable.
Fixed-stop core retained with visible on_demand_boarding_or_alighting limitation.
Continuous pickup/drop-off
Inventoried
Scheduled fixed-stop service remains routable. Continuous boarding or alighting between scheduled stops is not represented.
Fixed-stop core retained with visible limitation.
Loop routes
Yes
Sequence, not stop identity, orders a trip; reconstruction can retain repeated locations.
Supported, with adversarial regression coverage required.
Repeated stop in one trip
Yes
Distinct stop_sequence events remain distinct.
Supported.
block_id interlining
Inventoried
Each trip remains routable as ordinary scheduled service, but VIGO does not infer an in-seat continuation between trips sharing a block.
Included scheduled trips with a visible interlining limitation.
Duplicate stop, route, trip, or calendar IDs
Rejected
A primary-key conflict aborts the temporary build; no partial store is published.
Rejected malformed input with adversarial atomicity coverage.
Missing shape ID referenced by a trip
Yes
Timetable routing remains available, but published alignment is unavailable; any selected/QA stop-sequence line is dashed and marked inferred.
Timetable supported; published ride geometry unavailable.
Untimed interior stop-time gaps
Yes
Timed endpoints remain connected and the itinerary reports interpolated-stop-time-gap; no exact timestamp is claimed for omitted interior events.
Supported with degraded timing precision.
Malformed required table
Rejected
Build does not commit the temporary store.
Supported rejection.
Broken core references
Rejected
Unknown trip routes/services, parent stations, transfer endpoints, frequency trips, and stop-time trip/stop references abort the temporary build. Missing shapes remain a supported timetable-only case as declared above.
Rejected malformed input with adversarial atomicity coverage.
Semantic coverage written to store metadata
For otherwise importable GTFS, blockingRoutingFeatures currently has one stable code:
multiple_agency_timezones;
The following nonblocking routingLimitations codes put results in supported_scheduled_core mode:
frequency_based_service for exact_times=0 headway windows;
on_demand_boarding_or_alighting;
continuous_pickup_or_drop_off;
scoped_transfer_rules;
timed_transfer_guarantee;
in_seat_transfer_rules;
block_interlining.
The additional nonblocking code pathway_accessibility describes constraints not represented by the retained graph; it does not by itself change the coverage mode. “Nonblocking” does not mean full-feed support. It means VIGO can make a narrower, explicit claim over the compiled scheduled core instead of disabling unrelated service.
A ready result in supported_scheduled_core mode is optimal only over that compiled core. A no_path result in the same mode means no path exists in the core; an excluded trip or transfer rule could still change full-feed reachability.
Required fixture policy
Each row classified supported must have a miniature feed that would fail if the field were merely parsed. Each visible unsupported row must have a fixture that imports successfully and records the feature code. Permission fixtures must prove that the trip remains connected, illegal boarding and alighting events are rejected, and through-riding remains legal in the resident kernel, shared matrix path, and merged stores. Quarantined transfer fixtures must prove that the unsupported row is absent from the generic graph. Multiple-timezone fixtures must still prove a feed-wide blocked result. Rebuild an older SQLite store before using it for a published result.
Use a retained NDJSON request file containing public Route, Matrix, and Reach requests. Each line includes kind, the City or scenario selection supported by your service, the service date, and all routing constraints. Keep the file and prepared City together with their source checksums.
Set VIGO_API_TOKEN if the service requires authentication. The benchmark does not write the token to its report and refuses to overwrite an existing result. It calls the service's /v1/route, /v1/matrix, and /v1/reach endpoints.
The report retains every measured call, warmup calls, request-file hash, runtime capabilities, client platform, and optional unique-file City storage. Latency includes queueing, HTTP, response transfer, and JSON parsing. Quantiles use nearest rank. Successful results, valid no-journey results, HTTP failures, overload, service unavailability, timeouts, invalid responses, and transport failures are reported separately. Errors during warmup also fail the run.
For a cold-start measurement, start a fresh service and use --warmup 0 --rounds 1. Measure readiness separately from the first query. For sustained use, retain the same workload and increase rounds; report how much the workload repeats and whether caches are enabled. Compare equivalent inputs, constraints, output detail, hardware limits, and runtime builds.
City storage counts each file identity once, including shared scenario directories. It is logical file size, not physical APFS clone allocation. Server memory must be measured at the service: retain process-tree RSS/PSS or cgroup memory samples at startup, first query, repeated and concurrent queries, scenario switches, and peak large-output requests. Include the full application stack when claiming a deployment budget. The benchmark client's memory is not server memory.
Use public synthetic fixtures for CI and caller-owned Cities for operational acceptance. A short replay establishes behavior for that retained workload; it does not establish production reliability across unobserved traffic. See upgrading for installation and rollback checks.
This backend prototype is outside the main Network / Routes workspace. Its API, records and service tests remain available for research. The workflow below describes internal API actions, not additional Studio buttons or a supported public integration API.
The operational replay adds a synthetic holding decision through procedure selection, alternative comparison, approval, sandbox receipt and evidence-driven withdrawal. It reuses this ledger in separate replay storage. Internal knowledge and staff annotations are excluded from model context by default; approval alone does not authorize model disclosure.
The City operations ledger keeps findings, source evidence, staff decisions and rider guidance together. It works without a model. Internal tools can read approved public operational context and historical comparisons; mutations require explicit API requests from an authorized principal.
Handle a finding
Call operations-track for a current finding. The selected evidence, timetable identity, source references and quality summary are retained. Tracking the same event twice returns the existing record.
Use operations-transition to acknowledge the finding, investigate, and record action. Each transition requires a note. Link approved, unexpired context relevant to the finding's route or stop when useful.
Prepare rider guidance, or transition to monitoring. A missing event or stale feed leaves current availability unknown; it never resolves the case automatically.
Resolve with an explicit outcome and evidence reference: staff-confirmed recovery, false positive, or unable to confirm. Resolved cases can be reopened for investigation. These labels are staff assessments, not independently verified ground truth.
Evidence remains as captured until operations-refresh is used. A changed timetable requires a new finding against the new import. Every edit requires the current version; a stale client must reload before saving. Revision history preserves the preceding evidence and decisions.
Add shared context
knowledge-save accepts SOPs, maintenance records, document excerpts and operating notes. Each record has a source reference, scope and review date. New entries and revisions start as drafts. Approval applies to that exact version. Expired or draft material cannot be linked as approved guidance.
Keep source excerpts concise and identify their document revision or page. The API also supports exact stop scope. Content stays in the City ledger; no document crawler, embedding service or remote knowledge database is introduced. The internal operational_context tool retrieves up to five dated, versioned excerpts and labels their status. Retrieved text is treated as evidence, never executable instructions.
Prepare and deliver rider guidance
Call message-draft with the finding, channel, and audience to create an English template from its evidence. Channels have declared product limits: app and service alert, 2,000 characters; social, 280; signage, 160. These are editable starting templates, not assertions about every downstream platform's limits. Oversized templates require editing before approval; text is never silently truncated.
Audience choices are all riders, riders at a stop and accessible travel. Guidance asks riders to check departures or contact agency staff. It does not invent a disruption cause, recovery time, alternate route or guaranteed accessible connection.
Saving a revision clears approval. A reviewer checks the wording and evidence, approves the saved version, then releases it to the local outbox. Approval and release require current unchanged evidence, an action or monitoring state, and unchanged approved knowledge. Drafts initially expire after 15 minutes; the API permits an explicit expiry within 24 hours. A new draft is required when its source evidence changes.
message-release creates a durable local handoff containing the text, audience, channel, version, expiry, source evidence, and approval attribution. It does not transmit a message. After using the agency's own channel, staff can record its confirmation or public URL through message-delivery. That receipt is explicitly staff-recorded. message-withdraw preserves the prior revisions; staff must also remove an external copy in its channel. A repeated release request returns the same handoff rather than creating a duplicate.
Retain and compare history
The first received observation in each five-minute bucket is retained for up to 90 days, capped at 25,920 samples. Samples include source clocks, coverage, quality flags and route prediction summaries. Repeated UI reads do not increase sample counts. The app must be open and its existing feed refresh active; idle suspension, shutdown and missed intervals do not generate synthetic observations.
The history API compares a route with earlier service dates from the same timetable import, local weekday and hour. It first takes each day's median reported route maximum predicted departure delay, then the median across independent days. At least three earlier days are required. Insufficient coverage or sample counts remain unknown. A different import is excluded from the comparison. This is a descriptive prediction baseline, not actual vehicle performance, passenger waiting time or an anomaly detector.
The history API also reports an expanding-window evaluation: each historical day's target is compared only with earlier days. It returns the number of held-out days and mean absolute error; no current or future day enters its training subset. There is no trained ML predictor. Paginated sample and record endpoints expose retained observations, staff outcomes and revisions for subsequent analysis without claiming that those annotations establish causal effects.
Access, storage and operation
The shipped application remains a local, single-owner workspace. Its trusted host supplies local-owner with the admin role. No browser role selector, client-supplied identity or model tool can grant permissions. createAgencyService accepts a trusted access(projectId) callback for host-managed identity and City authorization; a missing or unauthorized identity fails closed.
Role
Capabilities
Viewer
Read records, evidence and history
Operator
Viewer access; track findings, record decisions, draft knowledge and messages
Reviewer
Operator access; approve another author's knowledge/message, release and record delivery
Admin
Reviewer access; configure connections; explicit same-author approval capability for the local owner
There is no hosted login, SSO, agency directory or multi-user deployment in this change. A hosting integration must authenticate each request and supply its authorized principal through the trusted boundary; exposing the default local-owner service as a shared server would not provide user isolation. Admin approval is attributed in the ledger, not disguised as an independent reviewer.
Each City owns agency/operations.sqlite, separate from the read-only timetable and existing notebook. A database records its owner and rejects reuse for a different City ID. Schema version 1 uses SQLite WAL, full synchronous commits, a bounded lock wait, indexed records and transactional audit writes. Newer schemas fail closed. Human records are capped at 10,000; capacity exhaustion reports an error instead of silently deleting decisions. Revision history is retained with human records. These are application-enforced audit records, not cryptographically tamper-proof storage against someone with filesystem access.
The quality summary exposes source freshness failures, unresolved trip reports, timetable coverage and missing comparable departure pairs. Its alignment fraction uses received reports as its denominator. It does not claim the percentage of scheduled trips observed. operations-health reports database integrity, schema, counts, sample policy, last retained time, refresh activity and refresh/storage errors. History write failures are visible in Agency warnings; previous records are preserved. Live feed failures retain the previous observation while its source clocks continue aging.
For a consistent backup, stop Studio and copy the entire City agency directory, including SQLite sidecar files if present. Retain the timetable with it to preserve evidence identity. Restore to the same City identity and verify operations-health before resuming. Moving or replacing the timetable invalidates comparisons that depended on its previous local identity. This feature does not add replication, continuous background hosting or a recovery-time guarantee.
API and verification
Use the existing POST /api/projects/:projectId/agency endpoint. Every mutation supplies id and version after creation. No mutation is in the model tool catalogue.
Lists return 50 records by default, up to 100, and accept query.before as the last returned ID (ascending IDs). Audit pages return 50 newest revisions and accept before as the last sequence. Observation pages return 100 newest samples and accept before as the last bucket. No external publication credentials are required or stored.
npm run check:agency includes the operations service fixture, covering workflow notes, message revision, approval, local release, recorded receipt, audit and insufficient history. node test/check-network-workspace-runtime.mjs checks the active Network workspace, keyboard tabs and responsive widths. All observations and receipts in these tests are synthetic. Runtime data remain in ignored temp/ storage.
The internal Running-time prediction review method and the historical_runtime tool read an existing MBTA LAMP study from the selected City's research directory at lamp/study.json. VIGO no longer includes the study-generation scripts, Python dependencies, or a retained example result.
The reader reports training and evaluation dates, coverage, matched timetable errors, and route or segment summaries from the saved report. A filtered route view retains an explicitly labeled study-wide comparison. Missing studies are reported as unavailable.
LAMP reconstructed stop events can include final arrival predictions. Saved results describe retrospective running-time comparisons, not independent sensor validation, live delay causes, recovery forecasts, or passenger impacts. MBTA results must not be applied to another agency because route names match.
The briefing starts with the distribution of conditions across reporting service. It then identifies shared locations, longer waits relative to the same scheduled departures, and what remains unreported. It is generated from the shared Agency observation and retained with its route and trip evidence.
The computed assessment appears immediately. It leads with the spread of late predictions, then prioritizes shared-area delays separately from longer rider waits. Whole-minute descriptions support the finding; quantiles and route counts remain evidence, not the headline. The network_overview tool exposes a compact version of the same diagnosis.
When a provider is configured and late predictions are present, a bounded investigation follows. The model chooses a focus and candidate explanations, then up to two additional evidence checks. Agency notices and upstream predictions are always checked. It assesses the resulting facts as plausible, weakened or unresolved, cites support and counterevidence, and chooses what to observe next. Two schema-constrained model calls have a shared 75-second deadline; no larger model is required. Invalid assessments retain the computed briefing and completed checks.
Measurements and denominators
Service window: the next 30 minutes, using the existing observation policy. Scheduled trips contribute only the seconds during which their indexed first departure–last arrival span intersects the window. Calendar exceptions and preceding service days with 24:00+ trips are included using the GTFS service clock. Frequency templates are explicitly excluded.
Reporting coverage: scheduled vehicle-minutes belonging to trips with a usable upcoming departure prediction or cancellation report, divided by all scheduled vehicle-minutes in the window. This weights frequent service by its actual scheduled supply. It is neither passenger coverage nor a health percentage.
Timetable deviation: one next departure per reporting trip instance. Median and 90th-percentile absolute deviations use empirical nearest ranks. Early, exactly matching, and late predictions are kept separate. Additional reporting trips outside the scheduled window remain in the timing distribution and are counted separately in coverage.
Spacing: only consecutive scheduled departures with both predictions at the same stop, direction and service date qualify. Equal intervals are retained. The same pair at multiple stops counts as one pair; its largest increase and stop extent are retained. Missing intermediate trips cannot become a measured service gap.
History: repeated late predictions require distinct source timestamps at the same trip and stop. The retained history supplies a measured duration and change, not an incident onset, actual passage history, or recovery forecast.
All indexed routes receive a state. No predictions means unknown; no scheduled trips means no scheduled service in this window. A single late prediction is distinguished from several late predictions. These are descriptive states, not hidden severity thresholds or claims about an agency's on-time standard. Exact matching predictions establish only the checked next departures.
Spatial interpretation
The geographic unit is an exact directed GTFS connection between stops. Overlapping scheduled-to-predicted departure windows on two or more routes identify a shared-location delay pattern. Adjacent segments connect only when the original segment observations have the same routes, overlapping windows, and a common affected trip. Connected components form the concentrations; an aggregate time envelope cannot create a new connection between otherwise separate observations. Each trip counts once in a concentration. Concentrations are ranked by summed positive departure delay across their distinct reporting trips, not the number of stop records or equal route weights.
This establishes where predicted lateness overlaps. It does not establish that delay originated on that segment, a corridor travel-speed reduction, propagation, a common incident cause, or passenger demand. Reverse-direction segments and non-overlapping time windows remain separate. Stops with similar names or nearby coordinates are not merged. Labels come from the timetable; no Boston neighborhoods are embedded in the method.
The implemented hierarchy is network → shared corridor/location → route → trip. Administrative regions, learned normal-variability ranges, passenger-weighted impact, and forecasts need additional agency data and validation. They are not fabricated to fill a five-layer diagram. Published alerts remain available in the shared observation and route views; the diagnosis does not infer a cause from their titles. The investigation can use an applicable notice as attributed evidence, with its stated route scope. Accessibility-only and no-effect notices are excluded from vehicle-delay explanations, while remaining available in the ordinary alerts views.
Bounded causal investigation
briefingInvestigation.mjs chooses and executes checks through the existing tool registry. serviceInvestigationEvidence.mjs uses the same immutable observation as the diagnosis. briefingInterpretation.mjs constructs public facts and realizes the model's selected explanation. The model can select hypotheses and evidence IDs; it cannot generate new numerical observations, incident labels, probabilities or unrestricted prose in this briefing path.
The candidate space is deliberately small: a shared corridor disruption, delay carried by individual trips, terminal/dispatch issues, or reporting inconsistency. The model can leave these unresolved. An upstream forecast can challenge a claim that delay began in the selected area, but does not establish an observed onset. Absent notices, missing vehicle positions and unavailable historical studies cannot be cited as support or counterevidence. Model-assessed plausible explanations precede unresolved or weakened ones. These remain working explanations, not calibrated causal classifications.
A retained observation was tested with the configured local qwen3.5:4b. Unrestricted prose invented a blockage and confused scheduled intervals with conditions elsewhere; an early constrained version misused escalator notices. Neither behavior was accepted. The revised form separates notice effects, requires upstream evidence, and preserves support and counterevidence. In the retained test, delay already forecast upstream weakened a corridor-origin explanation. This single case does not establish general model reliability.
The card shows the working explanation and next observation. An expandable investigation contains public evidence, not private model reasoning or JSON. Staff still need dispatch records, measured passage/speed data and corroborated incidents to establish a cause. The LAMP reader can inspect an existing historical study; it does not confirm current delay causes.
Refresh and saved evidence
The default is every 15 minutes while the network overview is open. Staff can choose 30 minutes, hourly, or manual. Preferences persist per City. Manual mode preserves the selected expiry interval; it stops automatic updates rather than declaring an old assessment current forever.
The server reuses a current assessment across tabs and coalesces concurrent generation requests. Manual refresh explicitly requests a new assessment. Server responses carry current preferences so a tab with older settings cannot keep refreshing an hourly or paused briefing every render.
Each card displays the assessment time, scheduled window and next refresh/expiry. Whole-minute headlines omit interval differences that would round to identical values; exact comparisons remain in the diagnosis. Expired cards, previous timetable revisions and loss of previously fresh prediction feeds do not retain a current-looking narrative. Saved originals remain readable and explicitly dated. A one-shot expiry timer and window-focus check handle idle tabs; there is no minute-by-minute model polling. Source conditions may change within a briefing interval: the displayed assessment time is the observation it describes, not a claim of continuous verification.
Implementation and verification
realtimeIntelligence.mjs retains complete measurements server-side. serviceWindow.mjs caches timetable spans per read-only context. serviceConcentrations.mjs, networkDiagnosis.mjs, and networkNarrative.mjs separate spatial grouping, aggregation and communication. briefingSchedule.mjs owns refresh validation and expiry. NetworkAssessment.tsx renders both the live card and saved evidence.
Run node test/check-network-diagnosis.mjs or npm run check:agency. Fixtures cover complete and incomplete coverage, equal intervals, one vote per trip, cancellations, stale and duplicate records, arrival-only updates, spatial overlap and separation, direction, no mutation of the shared observation, repeated source observations, overnight service, DST, caching, expiry, persistence and zero LLM calls for the computed briefing. UI, type, documentation and production build checks accompany the change.
Public references
GTFS Trip Updates explicitly distinguishes absent realtime information from on-time service and specifies trip-instance and stop-update semantics. The implementation preserves that distinction.
TransitMatters Data Dashboard 3.0 illustrates the value of service-level measurements and bus performance analysis. VIGO's current prediction-window assessment is not equivalent to its historical performance dataset; no parity or superiority claim is made.
VIGO Studio's replay API demonstrates a decision from source evidence to a withdrawn rider message. Run npm run evaluate:replay to exercise the backend sequence. There is no Replay panel in Studio. This uses a synthetic City X, separate storage, a fixed timetable and a controllable clock. It does not connect to dispatch or publish to riders.
The implementation reuses the operations ledger, finding transitions, knowledge approvals, message revisions and outbox. New modules supply replayable inputs, applicable procedure selection, holding comparisons and a sandbox transport. VIGO's routing engine and live network assessment are unchanged.
Replay sequence in the evaluation harness
Load the uneven-spacing scenario. Successive departures are predicted 3 and 17 minutes apart, against a 10-minute timetable. The selected bus is reported stopped at River control point.
Inspect the selected applicable procedure. The synthetic SOP limits holding to 180 seconds, requires a clear berth and completed boarding, protects the following headway and caps downstream delay. An expired section and another stop's procedure are excluded.
Compare no intervention, a target-headway baseline and the passenger-time optimizer. Prepare an option and review its rider message.
Approve the exact option and message. Send to the sandbox: the first attempt deliberately simulates a temporary transport failure. Retry records one receipt; repeated delivery requests cannot duplicate it.
Advance to the next observation. The bus is now reported in transit. The approved hold is no longer feasible, and its delivered sandbox message is withdrawn. Advancing the clock past expiry exercises a separate stale-clock case.
The replay clock pauses between actions. The 90-second decision lifetime uses that clock, not the operator's wall-clock reading time. Restart restores the active run and audit. New runs preserve previous records in the replay ledger; export a run before switching to retain its complete portable review package.
Synthetic case
No hold: modeled passenger-minutes
Baseline
Optimizer
Decision
Even service
400
0 seconds
0 seconds
No extra hold
Uneven spacing
596
180 s → 584 passenger-minutes
120 s → 580 passenger-minutes
Compare a shorter hold
Stale observations
Unknown
Unavailable
Unavailable
Obtain current evidence
Conflicting procedures
Not evaluated
Unavailable
Unavailable
Escalate
High onboard load
500
180 s → 716 passenger-minutes
0 s → 500 passenger-minutes
No extra hold
The high-load example illustrates the trade-off: restoring headways can worsen the modeled passenger outcome. These are simulated outcomes over a small downstream horizon, not measured passenger benefits or whole-route forecasts.
Data and procedure selection
The complete input package contains five cases: three development scenarios and two held-out variants. Expected actions are developer-authored, not expert agency labels, and are excluded from model prompts.
timetable.json contains the exact miniature GTFS tables: agency, route, stops, trips, stop times, dated calendar exceptions and feed version SYN-HOLD-1.
Each case preserves authored synthetic VehiclePosition and TripUpdate observations using a GTFS-Realtime JSON field mapping, including source/entity timestamps. These are not captured agency feeds or records reconstructed from the derived headways.
procedures.json preserves source passages, revisions, sections, effective periods, asset scope, prerequisites and authority. Its approval identities are synthetic.
The manifest fixes clocks, passenger inputs, filenames, expected actions and known unknowns. Raw source contents and versions determine equality; paths, modification times and digests do not identify the evidence. This establishes equality, not authenticity or tamper resistance.
This narrow replay adapter requires its explicit UTC timetable and date exceptions. Duplicate trip/stop matches, inconsistent service dates/directions, skipped stops and missing departures are rejected. It does not replace VIGO's production GTFS importer. A second agency needs real inputs, approved control points and an actual reviewer; changing a label would not establish transferability.
Replay records live in the City's agency/replay/operations.sqlite, separate from live operations. Export includes the package, candidate, message, receipt and revisions.
Knowledge records optionally carry structured procedure metadata. knowledge-select accepts exact route/stop IDs, query text and confirmed prerequisite identifiers; the server supplies the current clock. Selection removes unapproved, future, expired, out-of-scope, superseded and prerequisite-incomplete sections before SQLite FTS5/BM25 ranking. Conflicting limits or authority remain a conflict regardless of text rank. Results include the supporting passage and applicability explanation.
knowledge-save accepts and preserves structured metadata with revision/section information. Replay imports its versioned example directly. PDF ingestion, semantic retrieval and an expert-labelled retrieval study are not implemented. A lexical miss returns no matching section rather than substituting an unrelated procedure.
Method and assumptions
At each modeled downstream stop, a and b are predicted headways ahead and behind the selected bus, λ is the assumed passenger arrival rate, L is the supplied onboard load and h is additional holding in seconds:
The code evaluates the neighboring integer seconds and feasible endpoints of this convex objective. The interval intersects maximum holding, minimum following headway and maximum downstream delay constraints. Already-breached constraints, missing loads, stale sources or unconfirmed control-point presence return an unavailable result. Computation checks cancellation and a 250 ms deadline, with at most 50 downstream stops; no optimization server is needed.
The baseline is max(0, scheduled headway − current forward headway), clipped to the same constraints. It is a declared toy comparator, not an agency-agreed practice or an implementation of a named research model.
Assumptions: stationary uniform passenger arrivals; fixed running times; no overtaking; unlimited boarding capacity; constant supplied onboard load; no additional boarding during the control-point hold. The objective counts waiting over two headways at listed stops plus onboard holding. Capacity-denied boarding, dwell changes, transfers and network propagation are absent. No observed cause, recovery time, restricted-route alternative or accessible journey is established.
Optional review uses two model calls: choose an evidence check, then select an existing candidate or escalate. The server always supplies both the applicable procedure and computed alternatives. It does not spend a call choosing the only remaining mandatory check. Candidate IDs and evidence references are validated; the model cannot add durations, causes, recovery, approval or delivery. The internal compare_holding tool exposes the same comparison for an already-open synthetic case.
Actual staff context now defaults to internal. Internal SOPs, tracked finding notes and notebook annotations are excluded from model retrieval. Legacy answers that consulted internal-context tools without the current data policy are conservatively excluded from subsequent model history/retrieval. New answers produced under the public-context policy remain available. A document must be explicitly marked public, approved and unexpired before its excerpt may reach the model. Legacy approval alone does not authorize disclosure.
The replay model receives only public synthetic data and has no web or publishing tools. The public synthetic case and its selected evidence checks go to the configured inference endpoint. Localhost is not proof of local inference, forwarding policy, retention or security. This is a data-selection boundary, not security certification or erasure of material already transmitted by older versions.
Evaluation and reproduction
Shell
npm run evaluate:replay
node test/check-operational-replay.mjs
# With an explicitly configured VIGO_AGENCY_LLM_* connection:
npm run evaluate:replay -- --ai
Each evaluation writes results under output/replay/evaluations/. These generated reports are local outputs. The cases use synthetic inputs and developer-authored expectations; they do not establish staff decisions or observed service impact.