VIGO runs transit, walking, and driving queries from a standalone Rust executable. Use the command line for individual jobs or serve a prepared City over HTTP.
Start with Boston
Follow the Boston quickstart to route from Harvard Square to South Station. The data instructions show where to download the MBTA timetable and OpenStreetMap streets, and how to compile them into boston/. Then start a local service:
./vigo serve --city ./boston --port 8080
Open http://127.0.0.1:8080/ for this manual. The quickstart walks through a request and its response.
Guides and reference
Route Find a journey by transit, walking, or driving. Set a departure time or an arrival deadline.
Matrix Calculate travel times for many origin–destination pairs in one request.
Reach and isochrones Find streets and areas reachable by transit or walking within a time limit.
Scenarios Measure service changes with temporary exclusions, schedules, and overlays in Reach.
HTTP API Connect an application using JSON requests. Look up endpoints, limits, and status codes.
Deployment Configure authentication, containers, and process supervision for your own server.
Runtime and data
The executable includes the routing kernels, HTTP server, and SQLite. Network preparation uses the separate VIGO compiler. Once a City is prepared, queries need no Node, Python, or internet connection.
This manual covers VIGO 0.4.3. See Compatibility for supported operations, interface differences, and build information.
Quickstart
Start in Boston: route from Harvard Square in Cambridge to South Station in Boston, then ask when to leave to arrive by 09:00. A “City” is simply the prepared data directory that VIGO opens; here it is ./boston.
You need a matching Rust executable and the Boston directory. If you have only downloaded the executable, follow the Boston data instructions first: they give the MBTA timetable and OpenStreetMap download links and the exact build commands. Raw ZIP/PBF files are inputs to the separate compiler, not to ./vigo route.
Run the following from a working directory containing the executable and boston/. On Windows use vigo.exe.
./vigo --version
./vigo capabilities --pretty
./vigo info --city ./boston --pretty
Harvard Square to South Station
Save route.json. Coordinates are [longitude, latitude]. The service date below is Monday 2026-10-05, covered by the MBTA feed used to check this tutorial. For a newer download, choose a date inside its calendar coverage; see the feed inspection step below. These are scheduled journeys, without live delays.
The second command overrides the departure time with a 09:00 arrival deadline. Read status first: ok contains journey; not_found is a valid no-journey result. journey.departureTime and arrivalTime are service-day clocks. Durations use integer seconds. Read journey.legs and any route-specific warnings. Timetables and street data change; do not expect a permanently fixed trip ID or travel time.
Several origins and destinations
Save matrix.json. The rows are Harvard Square and Kendall Square; the columns are South Station and Copley Square. This requests all four pairs with their journeys:
durationsSeconds[row][column] follows the input order; null means no journey. For arrive-by matrices, duration is the arrival deadline minus latest departure; a journey can arrive before the deadline. Full journey detail is the default. For large analytical batches, journeyFormat: "compact" retains timed trip/stop witnesses with less display metadata; includeJourneys: false requests times only. Neither setting changes the routing search. Repeated rows or columns are shared internally, but every requested cell is returned. Compare full point responses and equivalent Matrix formats when timing the Node and Rust interfaces; full Rust matrices carry more metadata than Node's compact witnesses.
Keep Boston loaded for repeated requests
For a one-off answer, use route. For a school, accessibility study, or web application, keep Boston resident and send Route, Matrix, and Reach requests through the same process. Starting a new process for each route repeats City loading. For an application, use stream or serve and keep that process running. Save queries.ndjson, one object per line:
The default listener is 127.0.0.1:8080. Open http://127.0.0.1:8080/ for this offline manual. For production settings, see Deployment. Separate network-build time, fresh-process time to first answer, and resident request latency when measuring performance.
Installation
VIGO runs as one executable. Choose a package for your operating system and CPU, extract it, and point it at a prepared City.
Use a package
From the extracted directory, check the executable and inspect its supported operations:
The runtime includes SQLite and the routing kernels. A compiler, Node, and Python are not needed to run queries. City data is supplied separately; see City data, then continue with the quickstart.
Build from source
To build from this source checkout, install the pinned Rust toolchain in rust-toolchain.toml and a C toolchain for bundled SQLite. From the repository root:
Keep --no-default-features --features standalone to select the standalone executable. The default Cargo configuration builds the Node integration. Python is used by the packaging script.
For a different target, add --target RUST-TARGET-TRIPLE and provide its Rust standard library, linker, and C toolchain. City directories can be shared between compatible builds; the executable must match the destination operating system and CPU.
Package contents
The packaging script writes an archive to release/rust. It includes the executable, Markdown and HTML documentation, OpenAPI specification, test record, licenses, and manifest.json. The adjacent .sha256 file records the archive checksum.
The manifest identifies the build and hashes each file. Previous packages are saved under .previous. Platform test results and build provenance are described in Compatibility.
City data
A City is a compiled network containing the transit timetable, street graph, and access data. Build it once with the VIGO compiler, then copy the complete directory to the machine running the Rust executable.
Street paths for walking access, egress, transfers, and direct street routing
Use the static GTFS ZIP, not the GTFS-realtime protobuf. Use the OSM PBF, not the shapefile or GeoPackage. No MBTA API key is needed for these public file downloads. Preserve the source files and acquisition date for reproducibility; the latest URLs can change.
In a new working directory, using curl, unzip, and Osmium Tool:
feed_start_date and feed_end_date give the published feed interval. Check the weekday calendars and dated exceptions too: being inside that interval does not make every route run every day. The feed used for this tutorial spans 2026-09-25 through 2026-12-12; its example date is 2026-10-05, in America/New_York local service time.
For a smaller first build, extract central Boston, Cambridge, and nearby streets. Osmium's complete-ways strategy retains the nodes needed by ways crossing the boundary:
This rectangle supports the tutorial's four places; it is not full MBTA street coverage. For broader trips, use a larger extract covering every access, egress, and transfer location. You can skip Osmium and build with the full Massachusetts PBF instead; that is a larger preparation job and still does not cover the Rhode Island portion of MBTA service. Retain the source providers' required notices when distributing data; see OpenStreetMap attribution.
Compile Boston once
Use the Node CLI compiler, which imports raw data. The standalone Rust executable currently opens prepared data only. With an extracted CLI package, keep vigo.mjs and vigo-routing-kernel.node together and define:
Alternatively, build the compiler from a source checkout with Node 24.18+, npm, and the pinned Rust toolchain:
npm ci
npm run build:rust-routing-kernel
npm run build:cli
Then set vigo-build to node "/absolute/path/to/vigo/public/vigo.mjs" and run the same build from your Boston working directory. This avoids compiling Studio. Raw import and street preparation happen at build time; they are separate from query latency. Service timetables depend on the date and routing policy. Before distributing a City for repeated fresh-process use, prepare each service date you intend to use by running the compiler CLI once with the route.json from the quickstart:
Choose a date covered by your downloaded feed. Copy the complete City after this step. Rust reports timing.timetableSource: "prepared_snapshot" when it can reuse the matching timetable; otherwise it prepares from SQLite for that process and reports "source". Both paths use the same source schedule. A resident stream or serve process retains its active timetable. Compare startup only with identical date-specific prepared files; repeated Node invocations can write a missing snapshot, while Rust keeps the City read-only.
An existing output is rejected unless you explicitly pass --replace. Keep the completed boston/ directory beside your Rust executable, or pass its absolute path with --city. Continue with Harvard Square to South Station. No Node, Python, Osmium, or internet connection is needed for those Rust queries.
The final 0.4.3 pedestrian model requires a fresh build from the original inputs, including for Cities from earlier 0.4.3 candidates. See walking evidence for missing station costs, conservative access exclusions, and distance lower bounds. Preserve the runtime version and package checksum with the data.
Copy and load the City
The City includes network.json, routing/project.sqlite, prepared access artifacts, and an osm directory with street snapshots, access profiles, and CCH indexes. Exact member filenames can vary by preparation version. Copy the whole City, not just the SQLite files or manifest.
The loader checks source identities, snapshot formats, station/access profiles, and required CCH members. It rejects blocking source features, missing/stale preparations, and an unexpected routing SQLite WAL. Complete and checkpoint a City through its compiler; do not delete WAL files to bypass a validation error.
Update a network
Keep the directory immutable while processes use it. Compile updates into a new directory, start another service against it, verify readiness and a known query, switch traffic, then stop the old service. The runtime has no upload endpoint, source downloader, or hot-reload command.
vigo info reports name, revisionId, routing/street summaries, and warnings. A City revision identifies a dataset, not a guarantee of complete service or accurate realtime predictions. The runtime also needs prepared transit/access artifacts for walk-only queries; a bare street database is not a City.
Command line
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
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
--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
--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
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.
Points, time, and units
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.
{"coordinate": [-77.05, 38.9]}
{"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
true
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. The query does not automatically merge adjacent service dates. 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
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: "blocked".
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.
The journey departs at 07:55 and arrives at 08:30, with 25 minutes riding and 10 minutes waiting. transfers: 1 means two boardings. Times ending in Minutes use minutes; the native departure and arrival fields, when present, use service-day seconds. 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, requireTransitRide: false allows a direct OSM walk to compete with transit. 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 always materializes geometry in this version; includeGeometry is a Matrix option, not a Route option. Transit shape geometry is aligned using the shared native shape code. Missing source shapes fall back to a labeled stop sequence. Fare annotations and Studio's balanced/preference presentation are not part of this interface.
Via points and departure windows
Plan a journey through intermediate points or compare departures over a time window.
Via points
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 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.
Departure windows
windowMinutes accepts 0–240 (default 0). A positive value samples depart-at searches from the requested time, inclusive, at windowStepMinutes intervals (1–60, default 1). The result retains up to five distinct choices and records window.minutes, stepMinutes, and searches. This is a sampled search, not a continuous all-departures guarantee. Arrive-by windows are rejected. A nonempty via list takes precedence and removes window sampling from its segments; do not combine the two when you need departure alternatives.
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.
durationsMinutes[row][column] gives minutes 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. Walk/Drive matrices additionally return distancesMeters in the same layout. 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. If direct walking wins under requireTransitRide: false, its journey is a compact walk record. 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.
Reach and isochrones
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
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.
Read the surface
surface.values is a flat row-major array 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)
surface.fullValues uses surface.fullBounds 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.
Street evidence
Street evidence is surface.edges with schema vigo.standalone.street-edges.v1, encoding indexed-json. nodes is flat longitude/latitude pairs, endpoints contains two node indices per edge, and edgeIds, durationMinutes, walkDistanceM, and transitArrivalMinutes are parallel per-edge arrays. Use count and nodeCount for their domains. Inspect diagnostics.surface.edgeEvidenceTruncated before treating evidence as exhaustive. This encoding differs from Studio's binary edge bundles.
Scenarios
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 results
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.
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
deltaMinutes is after minus before for cells reachable in both. Negative means faster. commonCells, newlyReachableCells, noLongerReachableCells, and meanChangeMinutes distinguish changes in travel time from changes in reachability. Delta nulls are not zero changes. Mean change covers common cells only.
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.
Realtime and traffic
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:
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.
Route output
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 quickstart, 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 quickstart 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
station_selection
Selected-stop endpoint connector; no street path was checked for that leg
unverified_transfer
Endpoint line for which no street path was materialized
These labels apply to materialized transit legs. streetPathVerified: true identifies a graph-verified walking leg; false is explicit for the two connector fallbacks. 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 quickstart 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.
Matrix output
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 compact mode: "walk" journey with clocks, duration, distance, and transfers; no legs even if geometry was 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 output
Reach returns several views of the same computation: transit stop arrivals, a raster, area/contour GeoJSON, and optional street evidence. Use the representation that matches the question; a raster cell is not an individual Route query to its center.
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. Each finite value is the least elapsed time retained in that cell from reached graph nodes/edges; it does not promise a path to every point in the cell. Null means no finite value was retained under this computation's graph, grid, and limits.
This client function reads a cell without confusing zero with null:
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) {
throw new 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 for display, not a verified destination. 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.v1" 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 edge 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
Elapsed time to the directed edge's to endpoint on its retained best label; not the time to traverse just that edge
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 three measurement arrays have length count. The bundle retains fully traversable directed edges within the largest cutoff and walking budget. Opposite directions are separate records. To draw records whose to endpoint is reachable at a smaller cutoff, filter durationMinutes; that gives whole-edge evidence, not partial-edge interpolation.
function readEdge(bundle, i) {
if (!Number.isInteger(i) || i < 0 || i >= bundle.count) {
throw new RangeError('Edge is outside the bundle');
}
const point = node => bundle.nodes.slice(2 * node, 2 * node + 2);
const seed = bundle.transitArrivalMinutes[i];
return {
edgeId: bundle.edgeIds[i],
coordinates: [
point(bundle.endpoints[2 * i]),
point(bundle.endpoints[2 * i + 1])
],
durationMinutes: bundle.durationMinutes[i],
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. In this version, the high-level wrapper supplies a node-evidence limit of zero, which the kernel clamps to one: includeNodes: true returns at most one node and omits the native truncation flag. Use indexed edges for street coverage, or the native street.surface operation with an explicit node limit and its truncation diagnostic when you need node samples.
Comparison output
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 and native output
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 quickstart 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.
Results and errors
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.
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.
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.
Geometry is opt-in: send includeGeometry: true or use --include-geometry. Legs then include GeoJSON geometry. Default results retain distances and quality information without the coordinate arrays. Ordered journeys have a single ordered leg list; departure-window alternatives appear in alternatives.
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.
meta.computeUs retains the runtime's measured compute boundary, identified by computeScope. Rust measures dispatch including materialization; Node measures its routing call. Neither includes public formatting, final JSON serialization, HTTP queueing, or 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 runtime's query representation and City revision with output switches excluded. It is not a promise that equivalent aliases or different runtimes produce identical hashes. 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.
Compare and migrate
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.
Existing consumers of result, plan, top-level legs, durationMinutes, or surface.values must migrate to the public fields. For research tools that still require the original witness, explicitly request diagnostics: "trace" and read trace. The private Node worker protocol remains unchanged. Low-level native operations retain their separately documented contracts.
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.
Trace reference (debug only)
The rest of this section documents the internal object under trace, obtained only with diagnostics: "trace". 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:
{"error":{"code":"invalid_request","message":"serviceDate (YYYY-MM-DD) is required for transit"}}
The code invalid_request is currently generic, including transport failures. Use HTTP status and the message together; do not build categories by assuming every such code means invalid input. A computed blocked 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.
Streaming
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. Blank lines are ignored. There is no startup handshake or sequence field in the public Rust stream.
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 (4,096 shapes, 256 MiB of estimated storage, matching the Node interface) 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.
HTTP API
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/
Self-contained HTML manual
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
GET /v1/info
Loaded City summary
Bearer when configured
POST /v1/route
Route request/result
Bearer when configured
POST /v1/matrix
Matrix request/result
Bearer when configured
POST /v1/reach, /v1/isochrone
Reach request/result
Bearer when configured
POST /v1/compare
Saved Reach comparison
Bearer when configured
POST /v1/native
Typed native operation
Bearer when configured
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.
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
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.
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
404
Unsupported path or method
408
Request read deadline
413
Request body over limit
417
Unsupported Expect header
431
Header byte limit
503
Connection/queue 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. 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.
Deployment
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
A non-loopback bind requires a token at least 16 characters long. Set it through your deployment secret mechanism and send Authorization: Bearer .... 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.
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 City memory. There is no globally shared queue, distributed cache, tenant management, or server-side request persistence.
The final image is scratch, containing the static musl executable and licenses, running as UID/GID 65532. The documentation is embedded in the executable. Give this user read and traversal access to the mounted City. deploy/rust/compose.yaml provides equivalent mounting, token, read-only, and restart settings. Check Compatibility for platform validation status.
Process manager
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.
Client integration
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:
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.
Native API
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 included in the offline reader and archive.
Troubleshooting
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 the Rust manual/OpenAPI; Node CLI/Studio fields 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.
Compatibility
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 Intel/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.
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 generated HTML/specification, links/contracts, and 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 and regenerate the offline manual/OpenAPI with npm run docs:standalone. Native field schemas are derived from the Rust public structs. Check generated files before packaging so the embedded documentation describes the same executable.
Native fields
Use the standalone manual 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.
Selected arrival clock in service-day seconds, or null when no arrival is available.
Option<f64>
Always
bestBoardings
Option<u32>
Always
bestDestinationIndex
Option<u32>
Always
chainKinds
Vec<u32>
Always
chainFromStops
Vec<i32>
Always
chainToStops
Vec<i32>
Always
chainTripOrCandidate
Vec<i32>
Always
chainBoardSequences
Vec<f64>
Always
chainAlightSequences
Vec<f64>
Always
chainDurations
Vec<u32>
Always
chainArrivals
Vec<f64>
Always
queryNs
Native query time in nanoseconds; excludes outer transport work.
f64
Always
destinationSeedNs
f64
Always
originSeedNs
f64
Always
scanNs
f64
Always
chainNs
f64
Always
poppedStates
u32
Always
scannedDepartures
u32
Always
relaxedStops
u32
Always
expandedTripRuns
u32
Always
dominatedTripBoardings
u32
Always
explicitTransferChecks
u32
Always
Test record
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.
Walking evidence
VIGO 0.4.3 corrects underestimated stop-transfer walking times, omitted OSM node restrictions, and missing station coordinates coerced to (0, 0). These corrections apply to both Node Engine and standalone Rust through their shared prepared City data. Rebuild Cities produced by earlier 0.4.3 candidates 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. 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. If a pathway has no time, length, or distinct located endpoints, VIGO excludes it and reports 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 prepare it for the standalone executable as described in the Rust guide. 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 new street schema is v5. Routing-store schema v3 distinguishes missing coordinates and pathway costs from genuine zeros and requires a fresh GTFS import. Prepared access policy v6 connects street transfers to public entrances and composes their directed station pathways during preparation. 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:
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.