Glove
A fast multi-modal journey planner built in Rust with a React frontend.

Glove loads GTFS data into memory, builds a RAPTOR index, and exposes a Navitia-compatible REST API for journey planning. It supports public transit, walking, cycling, and driving via Valhalla integration.
The React portal provides an interactive map-based interface with autocomplete, route visualization, and multilingual support (FR/EN).
Get up and running in 3 commands:
bin/download.sh all # Download data
bin/valhalla.sh # Start Valhalla (optional)
bin/start.sh # Start the server
Then open http://localhost:8080
Key Features
Routing
- RAPTOR algorithm for optimal public transit journey computation
- Multi-modal: public transit, walking, cycling (3 profiles), driving
- Diverse alternatives with progressive pattern exclusion
- Journey tags: fastest, least transfers, least walking
- Elevation-colored bike routes (green = descent, red = climb)
- Turn-by-turn directions for walk, bike, and car routes
Data & Search
- Fuzzy autocomplete with French diacritics normalization
- BAN integration for French address geocoding
- Hot reload via API without service interruption
Frontend
- Interactive Leaflet map with route polylines and stop markers
- Mode tabs: Transit, Walk, Bike, Car
- Dark theme with CARTO basemap and glassmorphism UI
- Metrics panel with live CPU, memory, and request stats
Developer Experience
- Navitia-compatible API for drop-in replacement
- OpenAPI documentation auto-generated
- Prometheus metrics endpoint
- Benchmark tool for load testing
- Dev mode with cargo-watch + Vite HMR
Installation
Prerequisites
Download Data
Glove needs GTFS transit data to operate. The download script reads config.yaml for data URLs.
# Download everything (GTFS + OSM + BAN addresses)
bin/download.sh all
# Or download individually
bin/download.sh gtfs # GTFS transit schedules
bin/download.sh osm # OpenStreetMap data (for Valhalla)
bin/download.sh ban # BAN French addresses (for autocomplete)
By default, this downloads data for Ile-de-France (Paris region). You can change the data URLs in config.yaml to use GTFS feeds from other regions.
Start Valhalla (Optional)
Valhalla provides walking, cycling, and driving directions. Without it, only public transit routing is available.
bin/valhalla.sh # Pulls Docker image, builds tiles, starts on port 8002
This creates a Docker container named valhalla that builds routing tiles from the downloaded OSM data.
Run
Production Mode
bin/start.sh
This builds the Rust backend in release mode, builds the React frontend, and starts the server on port 8080.
Development Mode
bin/start.sh --dev
This starts:
- Backend:
cargo-watchfor automatic recompilation on Rust file changes - Frontend: Vite dev server with HMR on port 3000
Manual Start
# Terminal 1 — Backend
cargo run --release
# Terminal 2 — Frontend
cd portal && npm install && npm run dev
Access
| Service | URL |
|---|---|
| Portal (production) | http://localhost:8080 |
| Portal (dev mode) | http://localhost:3000 |
| API | http://localhost:8080/api |
| OpenAPI spec | http://localhost:8080/api-docs/openapi.json |
Configuration
Glove is configured via config.yaml at the repository root. All settings have sensible defaults.
Server
server:
bind: "0.0.0.0"
port: 8080
workers: 1 # 0 = auto (one per logical CPU)
log_level: "info" # trace, debug, info, warn, error
shutdown_timeout: 30 # seconds — graceful shutdown for in-flight requests
api_key: "" # Required for POST /api/gtfs/reload. Empty = endpoint disabled
cors_origins: [] # Allowed origins. ["*"] = permissive (not for production)
rate_limit: 20 # Max requests/sec per IP. 0 = disabled
| Setting | Description | Default |
|---|---|---|
bind | Network interface to listen on | 0.0.0.0 |
port | HTTP port | 8080 |
workers | Actix worker threads. 0 = one per CPU core | 1 |
log_level | Minimum log level | info |
shutdown_timeout | Seconds to wait for in-flight requests on shutdown | 30 |
api_key | API key for the reload endpoint. Empty disables the endpoint | "" |
cors_origins | List of allowed CORS origins. ["*"] allows all | [] |
rate_limit | Maximum requests per second per IP address | 20 |
Data Sources
data:
dir: "data"
gtfs_url: "https://data.iledefrance-mobilites.fr/..."
osm_url: "https://download.geofabrik.de/europe/france/ile-de-france-latest.osm.pbf"
ban_url: "https://adresse.data.gouv.fr/data/ban/adresses/latest/csv"
departments: [75, 77, 78, 91, 92, 93, 94, 95]
| Setting | Description |
|---|---|
dir | Base data directory. Sub-directories gtfs/, osm/, raptor/, ban/ are created automatically |
gtfs_url | URL to download the GTFS zip archive |
osm_url | URL to download the OpenStreetMap PBF file (for Valhalla) |
ban_url | Base URL for BAN address CSV files |
departments | French department codes to download BAN data for |
Routing
routing:
max_journeys: 5
max_transfers: 5
default_transfer_time: 120 # seconds
max_duration: 10800 # 3 hours in seconds
max_nearest_stop_distance: 1500 # meters (~20 min walk at 5 km/h)
| Setting | Description | Default |
|---|---|---|
max_journeys | Maximum number of alternative journeys to return | 5 |
max_transfers | Maximum number of transfers in a journey | 5 |
default_transfer_time | Default walking time between stops (seconds) | 120 |
max_duration | Maximum total journey duration (seconds) | 10800 (3h) |
max_nearest_stop_distance | Maximum distance to nearest stops (meters) | 1500 |
Valhalla
valhalla:
host: "localhost"
port: 8002
The Valhalla routing engine is used for walking, cycling, and driving directions. It runs as a separate Docker container. When OSM data includes indoor information, Valhalla provides indoor maneuvers (elevator, stairs, escalator, enter/exit building) in transfer and walking sections.
Map
map:
zoom: 11
center_lat: 48.8566
center_lon: 2.3522
bounds_sw_lat: 48.1
bounds_sw_lon: 1.4
bounds_ne_lat: 49.3
bounds_ne_lon: 3.6
tile_url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
tile_cache_duration: 864000 # seconds (10 days)
These settings control the initial map view, geographic bounds, and the tile caching proxy.
| Setting | Description | Default |
|---|---|---|
zoom | Default map zoom level | 11 |
center_lat / center_lon | Default map center | 48.8566 / 2.3522 (Paris) |
bounds_sw_* / bounds_ne_* | Geographic bounds (SW and NE corners) | Île-de-France |
tile_url | Upstream tile server URL template. Placeholders: {s} (subdomain), {z}, {x}, {y}, {r} (retina) | CARTO Voyager |
tile_cache_duration | Browser cache duration for tiles (seconds) | 864000 (10 days) |
Tiles are fetched from the upstream server on first request and cached to data/tiles/ on disk. Subsequent requests are served from cache.
Bike Profiles
bike:
city:
cycling_speed: 16.0 # km/h
use_roads: 0.2 # prefer bike lanes
use_hills: 0.3 # avoid climbs
bicycle_type: "City"
ebike:
cycling_speed: 21.0
use_roads: 0.4
use_hills: 0.8 # climbs are easy with motor
bicycle_type: "Hybrid"
road:
cycling_speed: 25.0
use_roads: 0.6
use_hills: 0.5
bicycle_type: "Road"
Three bike profiles are available, each with independent Valhalla routing parameters:
| Profile | Speed | Use Case |
|---|---|---|
| City | 16 km/h | Velib' / city bikes, avoids hills and busy roads |
| E-bike | 21 km/h | Electric bikes (VAE), handles hills easily |
| Road | 25 km/h | Road bikes, prefers smooth tarmac |
Wheelchair Accessibility
wheelchair:
step_penalty: 999999 # effectively avoid stairs
max_grade: 6 # 6% slope max (wheelchair norms)
use_hills: 0.0 # avoid hills entirely
elevator_penalty: 0 # prefer elevators
walking_speed: 3.5 # km/h — typical wheelchair speed
These settings are used when the wheelchair=true parameter is passed to journey endpoints. They configure Valhalla's pedestrian costing model for wheelchair-accessible routing.
| Setting | Description | Default |
|---|---|---|
step_penalty | Penalty for stairs. Very high value effectively avoids them | 999999 |
max_grade | Maximum road grade in percent (6% is the standard wheelchair norm) | 6 |
use_hills | Hill avoidance factor (0.0 = strongly avoid, 1.0 = no preference) | 0.0 |
elevator_penalty | Penalty for elevators (0 = prefer them) | 0 |
walking_speed | Wheelchair speed in km/h | 3.5 |
When wheelchair mode is active, the walking speed slider in the frontend is locked to the configured wheelchair speed (3.5 km/h), and bike/car modes are hidden.
Docker
Glove provides a multi-stage Dockerfile for containerized deployment.
Build the Image
docker build -f docker/Dockerfile -t glove .
The build uses three stages:
- Node.js (node:20-alpine) — builds the React frontend with Vite
- Rust (rust:1.87) — compiles the backend in release mode
- Runtime (debian:bookworm-slim) — minimal image with just the binary and static files
Run
docker run -d \
--name glove \
-p 8080:8080 \
-v $(pwd)/data:/app/data \
-v $(pwd)/config.yaml:/app/config.yaml \
glove
The container:
- Exposes port 8080
- Needs the
data/directory mounted with GTFS data - Needs
config.yamlmounted for configuration - Includes a healthcheck on
GET /api/status
Valhalla Container
For walk/bike/car routing, Valhalla runs as a separate container:
bin/valhalla.sh
This script:
- Pulls the
ghcr.io/valhalla/valhallaDocker image - Builds routing tiles from the downloaded OSM data
- Starts the container on port 8002
The Valhalla configuration includes:
include_platforms=Trueto import platform/indoor data from OSMstep_penaltyandelevator_penaltyin pedestrian costing to fine-tune indoor routing preferences- Indoor maneuver support (elevator, stairs, escalator, enter/exit building) when OSM data is available
Make sure config.yaml points to the Valhalla host:
valhalla:
host: "localhost" # or the Docker container name if using Docker networking
port: 8002
Docker Compose (Example)
version: "3.8"
services:
glove:
build:
context: .
dockerfile: docker/Dockerfile
ports:
- "8080:8080"
volumes:
- ./data:/app/data
- ./config.yaml:/app/config.yaml
depends_on:
- valhalla
valhalla:
image: ghcr.io/valhalla/valhalla:latest
ports:
- "8002:8002"
volumes:
- ./data/osm:/custom_files
Adjust valhalla.host in config.yaml to valhalla (the service name) when using Docker Compose networking.
Architecture Overview
Glove is a monorepo with a Rust backend and React frontend.
High-Level Architecture
Design Principles
There is no database. All GTFS data is loaded from CSV files at startup and held in memory. This gives extremely fast query times at the cost of startup time (10-30 seconds for index building).
The RAPTOR index is wrapped in ArcSwap, which allows atomic pointer swaps. When new GTFS data is loaded via POST /api/gtfs/reload, the entire index is rebuilt in a background thread and swapped in atomically. No request is ever blocked or sees partial data.
Trips with identical stop sequences are grouped into patterns. This dramatically reduces memory usage and speeds up the RAPTOR scan phase, because the algorithm only needs to evaluate one entry per pattern instead of one per trip.
Valhalla supports indoor maneuvers such as elevators, stairs, escalators, and building enter/exit transitions. Transfers are classified by parent_station: outdoor transfers (different parent_station) always get a Valhalla walking route for the map polyline, while indoor transfers (same parent_station) are only enriched when indoor maneuvers exist in OSM. Transfer polylines use the Valhalla shape (actual walking route) when available, falling back to a straight line otherwise. This enrichment only runs when maneuvers=true is requested.
Technology Stack
| Component | Technology |
|---|---|
| Backend | Rust, Actix-web 4 |
| Routing | RAPTOR algorithm (custom implementation) |
| Walk/Bike/Car | Valhalla (Docker, with indoor routing support) |
| Frontend | React 19, Vite, MUI 7, Leaflet |
| Data format | GTFS (General Transit Feed Specification) |
| Address search | BAN (Base Adresse Nationale) |
| Serialization | serde (JSON + YAML + CSV) |
| API docs | utoipa (OpenAPI auto-generation) |
| Monitoring | Custom Prometheus metrics |
| Logging | tracing + tracing-subscriber |
RAPTOR Algorithm
RAPTOR (Round-bAsed Public Transit Optimized Router) is the core routing algorithm in Glove. It finds Pareto-optimal journeys that minimize both arrival time and number of transfers.
How It Works
Rounds
RAPTOR operates in rounds. Each round allows one additional vehicle trip:
- Round 0: Walk from the origin to nearby stops
- Round 1: Take one transit vehicle (no transfers)
- Round 2: Take up to two vehicles (one transfer)
- Round k: Take up to k vehicles (k-1 transfers)
The algorithm stops when no improvement is found or max_transfers is reached.
Within Each Round
For each round, RAPTOR:
- Collects marked stops — stops that were improved in the previous round
- Scans patterns — for each route pattern passing through a marked stop, finds the earliest trip that can be boarded and propagates arrival times along the remaining stops
- Transfers — from every newly improved stop, walks to neighboring stops using the transfer graph
Labels
Each stop maintains a label per round: the earliest known arrival time. Labels also store back-pointers for journey reconstruction (which trip, which boarding stop, etc.).
Pre-Processing
On startup (10-30 seconds), Glove builds several indexes:
| Index | Purpose |
|---|---|
| Stop spatial index | Maps coordinates to nearby stops within max_nearest_stop_distance |
| Service ID interning | Converts string service IDs to integers for fast calendar lookups |
| Pattern grouping | Groups trips with identical stop sequences into patterns |
| Transfer graph | Precomputes walking transfers between nearby stops |
| Calendar index | Maps dates to active service IDs using GTFS calendar + calendar_dates |
The index is serialized to disk (data/raptor/) with a fingerprint. On subsequent startups, if the GTFS data hasn't changed, the cached index is loaded directly (sub-second startup).
Diverse Alternatives
Glove returns multiple alternative journeys using iterative pattern exclusion:
- Run RAPTOR and collect all Pareto-optimal journeys from the result
- For each journey found, record which patterns were used
- Run RAPTOR again, excluding previously used patterns
- Repeat until
max_journeysis reached or no new journeys are found
This ensures diverse alternatives that use genuinely different routes, not just minor time variations.
Service Filtering
RAPTOR is calendar-aware. For each query date:
- The active services are determined from
calendar.txt(day-of-week rules + date ranges) - Exceptions from
calendar_dates.txtare applied (additions and removals) - Only trips belonging to active services are considered during the scan
Fuzzy Stop Search
The autocomplete endpoint uses a ranked fuzzy search with French diacritics normalization:
- Exact match (highest priority)
- Prefix match (stop name starts with query)
- Word-prefix match (any word in the stop name starts with query)
- Substring match (query appears anywhere in the stop name)
Diacritics are normalized (e.g., "gare de l'est" matches "Gare de l'Est") via a custom normalization function.
Key Optimizations
- Binary search in
find_earliest_tripfor O(log n) trip lookup within patterns - Pre-allocated buffers reused across rounds (no per-round allocation)
- FxHashMap for stop index and calendar exceptions (faster than default HashMap)
- Pareto-optimal exploitation — all Pareto-optimal journeys from a single RAPTOR run are used before re-running
- Cache persistence — serialized index with fingerprint-based invalidation
Data Flow
Startup Sequence
GTFS Data Model
Glove loads the following GTFS files:
| File | Content | Rust Struct |
|---|---|---|
agency.txt | Transit agencies | Agency |
routes.txt | Transit routes (lines) | Route |
stops.txt | Stop locations | Stop |
trips.txt | Individual trips | Trip |
stop_times.txt | Arrival/departure at each stop | StopTime |
calendar.txt | Weekly service schedules | Calendar |
calendar_dates.txt | Service exceptions | CalendarDate |
transfers.txt | Transfer connections between stops | Transfer |
Query Flow
Public Transit Journey
Walk / Bike / Car Journey
Hot Reload
The hot reload mechanism allows updating GTFS data without downtime:
POST /api/gtfs/reloadis called (requiresapi_key)- A background thread loads new GTFS data and builds a fresh RAPTOR index
- The new index is swapped in atomically via
ArcSwap - All in-flight requests continue using the old index until they complete
- The old index is dropped when the last reference is released
Transfer Enrichment
When maneuvers=true is requested, transfer sections in public transport journeys are enriched with Valhalla walking routes. The enrichment logic depends on the transfer type:
- Outdoor transfers (stops with different
parent_station): Valhalla is always called to obtain the actual walking route shape, which is displayed on the map as a polyline. - Indoor transfers (stops sharing the same
parent_station): Valhalla is only called when indoor maneuvers (elevator, stairs, escalator) exist in OSM data for that station. - Polyline rendering: transfer sections use the Valhalla shape (actual walking route) when available; otherwise a straight line is drawn between the two stops.
Transfer enrichment calls are batched in parallel using futures::join_all for performance. When maneuvers is not requested (the default), transfer Valhalla enrichment is skipped entirely
Frontend
The Glove frontend is a single-page React application with an interactive map.
Technology Stack
| Library | Version | Purpose |
|---|---|---|
| React | 19 | UI framework |
| Vite | - | Build tool with HMR |
| MUI (Material-UI) | 7 | Component library |
| Leaflet + react-leaflet | - | Interactive map |
| Swagger UI React | - | API documentation viewer |
Layout
The UI consists of two main areas:
- Left sidebar: search form, journey results, settings panel, metrics
- Right area: full-height Leaflet map with route visualization
All components live in a single file portal/src/App.jsx for simplicity.
Features
Mode Tabs
Four transport modes are available as tabs:
- Transit — Public transport via RAPTOR
- Walk — Pedestrian routing via Valhalla
- Bike — Cycling with 3 profiles (City, E-bike, Road)
- Car — Driving via Valhalla
Transit and Walk/Bike/Car queries are sent in parallel; results are displayed as they arrive.
Transport Mode Labels
The frontend displays real commercial names for transit lines rather than generic mode names. For example:
- RER A instead of "rail A"
- Transilien H instead of "rail H"
- TER for regional trains
- Metro 4 instead of "subway 4"
This provides a familiar experience for users of the Ile-de-France transit network.
Settings Panel
The settings panel is organized into three titled sections, each with an icon:
- Walking Speed (DirectionsWalk icon) — adjusts walking speed for transit journey calculations
- Transport Modes (Commute icon) — select which transit modes to include
- Advanced Options (Tune icon) — includes:
- Show detailed instructions switch — enables turn-by-turn maneuvers (
maneuvers=true) in journey responses. Disabled by default - Wheelchair accessible switch — enables wheelchair-accessible routing. When active, walking speed is locked to 3.5 km/h, bike and car modes are hidden, and the
most_accessiblejourney tag is displayed
- Show detailed instructions switch — enables turn-by-turn maneuvers (
Search & Autocomplete
The search form provides:
- Origin and destination fields with fuzzy autocomplete
- Date/time picker
- Swap origin/destination button
- Results appear ranked: stops first, then addresses
Map Visualization
- Route polylines colored by transport mode
- Stop markers with popups showing stop names and departure/arrival times
- Origin (green) and destination (red) bubbles
- Bike routes colored by elevation gradient (green = descent, red = climb)
Dark Theme
The app uses a dark theme by default with:
- CARTO Dark Matter basemap tiles
- Glassmorphism UI effects (translucent sidebar)
- MUI dark palette
Internationalization
Two languages are supported via portal/src/i18n.jsx:
- French (default, auto-detected)
- English
The language is detected from the browser's locale and can be toggled in the UI.
Metrics Panel
A collapsible metrics panel shows live server statistics:
- CPU and memory usage
- Uptime
- HTTP request counts and error rates
- GTFS data stats (stops, routes, trips)
Map Bounds
The map is constrained to the configured geographic bounds (default: Ile-de-France) to prevent users from searching outside the coverage area.
API Endpoints
Glove exposes a REST API on the configured port (default: 8080). All endpoints return JSON.
Endpoint Summary
| Method | Path | Description |
|---|---|---|
GET | /api/journeys/public_transport | Public transit journey planning (RAPTOR) |
GET | /api/journeys/walk | Walking directions (Valhalla) |
GET | /api/journeys/bike | Cycling directions (Valhalla, 3 profiles) |
GET | /api/journeys/car | Driving directions (Valhalla) |
GET | /api/places | Stop and address autocomplete |
GET | /api/status | GTFS stats and server status |
GET | /api/gtfs/validate | GTFS data quality validation (19 checks) |
POST | /api/gtfs/reload | Hot-reload GTFS data |
GET | /api/metrics | Prometheus-format metrics |
GET | /api/tiles/{z}/{x}/{y}.png | Map tile proxy with local disk cache |
GET | /api-docs/openapi.json | OpenAPI specification |
Navitia Compatibility
The journey planning endpoints use query parameters compatible with the Navitia API:
from— Origin coordinates (lon;lat)to— Destination coordinates (lon;lat)datetime— Departure time (ISO 8601)
This allows Glove to serve as a drop-in replacement for Navitia in existing applications.
All journey endpoints accept an optional maneuvers=true query parameter. When enabled, responses include a maneuver_type field (Valhalla type number) in maneuver objects, enabling clients to display turn-by-turn navigation with indoor maneuver support. Maneuvers are disabled by default to reduce response size and skip transfer Valhalla enrichment.
Authentication
Most endpoints are public. The POST /api/gtfs/reload endpoint requires an API key configured in config.yaml:
server:
api_key: "your-secret-key"
Pass the key in the Authorization header:
curl -X POST http://localhost:8080/api/gtfs/reload \
-H "Authorization: Bearer your-secret-key"
Rate Limiting
All endpoints except the tile proxy are rate-limited per IP address. The default is 20 requests/second, configurable via:
server:
rate_limit: 20 # 0 = disabled
CORS
CORS is configured via config.yaml:
server:
cors_origins: [] # Default: restrictive
cors_origins: ["*"] # Permissive (not for production)
cors_origins: ["https://example.com"] # Specific origins
OpenAPI Documentation
The full API specification is auto-generated and available at:
GET /api-docs/openapi.json
The frontend includes a Swagger UI viewer for interactive API exploration.
Journey Planning
Public Transit
GET /api/journeys/public_transport
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Origin coordinates (lon;lat) |
to | string | Yes | Destination coordinates (lon;lat) |
datetime | string | No | Departure time (ISO 8601, e.g. 20240315T083000). Defaults to now |
datetime_represents | string | No | Whether datetime is departure (default) or arrival |
maneuvers | bool | No | Include turn-by-turn maneuvers in response (default: false). When absent, maneuvers are omitted and transfer Valhalla enrichment is skipped |
wheelchair | bool | No | Enable wheelchair-accessible routing (default: false). Avoids stairs, limits slope, prefers elevators. Adds most_accessible journey tag |
forbidden_modes | string | No | Comma-separated commercial modes to exclude (e.g. metro,bus,rail) |
forbidden_uris[] | string | No | Route IDs to exclude from routing |
walking_speed | float | No | Walking speed override in m/s (default: ~1.12 m/s = 4 km/h) |
max_nb_transfers | int | No | Maximum number of transfers allowed |
min_nb_transfers | int | No | Minimum number of transfers |
max_duration | int | No | Maximum journey duration in seconds |
max_walking_duration_to_pt | int | No | Maximum walking time to reach transit (seconds) |
first_section_mode[] | string | No | Modes allowed for the first leg (e.g. walking, bike, car) |
last_section_mode[] | string | No | Modes allowed for the last leg |
direct_path | string | No | Include direct non-transit path (none, only) |
count | int | No | Number of journeys requested |
max_nb_journeys | int | No | Maximum number of journeys in response |
Example
curl "http://localhost:8080/api/journeys/public_transport?\
from=2.3522;48.8566&\
to=2.2945;48.8584&\
datetime=20240315T083000"
Response
The response follows the Navitia journey format:
{
"journeys": [
{
"departure_date_time": "20240315T083000",
"arrival_date_time": "20240315T090500",
"duration": 2100,
"nb_transfers": 1,
"tags": ["fastest"],
"sections": [
{
"type": "street_network",
"mode": "walking",
"duration": 300,
"geojson": { ... },
"maneuvers": [
{
"instruction": "Walk south on Rue de Rivoli.",
"maneuver_type": 2
}
]
},
{
"type": "public_transport",
"display_informations": {
"commercial_mode": "Metro",
"code": "1",
"direction": "La Défense",
"color": "FFCD00"
},
"from": { "name": "Châtelet", ... },
"to": { "name": "Charles de Gaulle - Étoile", ... },
"departure_date_time": "20240315T083500",
"arrival_date_time": "20240315T085000",
"geojson": { ... },
"stop_date_times": [ ... ]
},
{
"type": "transfer",
"duration": 180,
"maneuvers": [
{
"instruction": "Take the elevator to level 0.",
"maneuver_type": 37
}
]
},
{
"type": "public_transport",
...
}
]
}
]
}
Journey Tags
Each journey may have one or more tags:
fastest— Shortest total durationleast_transfers— Fewest number of transfersleast_walking— Least total walking time, including both street_network sections (first/last mile) and transfer durationsmost_accessible— (wheelchair mode only) Least walking + fewest transfers, best for wheelchair users
Maneuvers
Maneuvers are disabled by default. To include them, pass ?maneuvers=true on any endpoint. When enabled, street network sections and transfer sections include a maneuvers array with turn-by-turn directions. Each maneuver contains:
| Field | Description |
|---|---|
instruction | Human-readable direction text |
maneuver_type | Valhalla maneuver type number (e.g., 2 = turn right, 37 = elevator, 38 = stairs, 39 = escalator) |
Transfer sections only include maneuvers when indoor routing data is available from OSM. Indoor maneuver types include elevator (37), stairs (38), escalator (39), enter building (40), and exit building (41).
The maneuver_type field is a Valhalla type number included in all walk, bike, and car responses, as well as in street_network and transfer sections of public transport responses.
Walking
GET /api/journeys/walk
Uses Valhalla for pedestrian routing.
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Origin (lon;lat) |
to | string | Yes | Destination (lon;lat) |
maneuvers | bool | No | Include turn-by-turn maneuvers (default: false) |
wheelchair | bool | No | Wheelchair-accessible routing: avoids stairs, limits slope to 6%, speed 3.5 km/h |
Cycling
GET /api/journeys/bike
Uses Valhalla with configurable bike profiles.
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Origin (lon;lat) |
to | string | Yes | Destination (lon;lat) |
profile | string | No | Bike profile: city, ebike, or road (default: city) |
maneuvers | bool | No | Include turn-by-turn maneuvers (default: false) |
The response includes elevation data and maneuver-by-maneuver directions. The frontend uses elevation data to color the route polyline (green = descent, red = climb).
Driving
GET /api/journeys/car
Uses Valhalla for driving directions.
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Origin (lon;lat) |
to | string | Yes | Destination (lon;lat) |
maneuvers | bool | No | Include turn-by-turn maneuvers (default: false) |
Wheelchair Accessible Routing
All journey endpoints that use Valhalla (public transit, walk) support a wheelchair=true parameter. When enabled:
- Stairs are avoided — Step penalty set extremely high (999999)
- Slope is limited — Maximum grade 6% (wheelchair norm)
- Hills are avoided — Use hills factor set to 0.0
- Elevators are preferred — Elevator penalty set to 0
- Speed is reduced — Walking speed fixed at 3.5 km/h (typical wheelchair speed)
For public transit, wheelchair mode also adds the most_accessible journey tag to the result with the fewest transfers and least walking time.
In the frontend, the wheelchair toggle in the settings panel automatically enables this mode and disables the walking speed slider (fixed at 3.5 km/h). Bike and car modes are hidden when wheelchair mode is active.
Tile Caching Proxy
GET /api/tiles/{z}/{x}/{y}.png
Proxies map tile requests to a configurable upstream tile server and caches tiles locally on disk under data/tiles/{z}/{x}/{y}.png. Subsequent requests are served from cache.
| Parameter | Type | Description |
|---|---|---|
z | integer | Zoom level (0–20) |
x | integer | Tile column |
y | integer | Tile row |
The upstream server URL template and browser cache duration are configured in config.yaml:
map:
tile_url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
tile_cache_duration: 864000 # seconds (10 days)
Placeholders: {s} (subdomain a/b/c/d for load balancing), {z}, {x}, {y}, {r} (retina).
Places & Autocomplete
GET /api/places
Search for transit stops and addresses with fuzzy matching.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (e.g. "gare de lyon") |
limit | integer | No | Maximum number of results (default: 10, max: 50) |
Example
curl "http://localhost:8080/api/places?q=chatelet"
Response
{
"places": [
{
"id": "stop_point:IDFM:12345",
"name": "Châtelet",
"embedded_type": "stop_point",
"stop_point": {
"id": "stop_point:IDFM:12345",
"name": "Châtelet",
"coord": {
"lat": "48.858370",
"lon": "2.347000"
}
}
},
{
"id": "address:75001_chatelet",
"name": "Place du Châtelet, Paris",
"embedded_type": "address",
"address": {
"name": "Place du Châtelet",
"coord": {
"lat": "48.858200",
"lon": "2.347100"
}
}
}
]
}
Search Ranking
Results are ranked by match quality:
- Exact match — "Châtelet" matches "Châtelet" (highest priority)
- Prefix match — "chat" matches "Châtelet"
- Word-prefix match — "lyon" matches "Gare de Lyon"
- Substring match — "elet" matches "Châtelet" (lowest priority)
Transit stops are always prioritized over BAN addresses in the results.
Diacritics Normalization
The search handles French diacritics transparently:
chateletmatchesChâteletgare de l'estmatchesGare de l'EstoperamatchesOpéra
Status & Reload
Status
GET /api/status
Returns GTFS data statistics and server information. No authentication required.
Response
{
"status": "ok",
"gtfs": {
"agencies": 42,
"routes": 1850,
"stops": 48000,
"trips": 320000,
"stop_times": 8500000,
"calendars": 2500,
"calendar_dates": 15000,
"transfers": 95000
},
"last_load": "2024-03-15T08:00:00Z"
}
This endpoint is used as the Docker healthcheck.
GTFS Validation
GET /api/gtfs/validate
Runs 19 automated data quality checks on the loaded GTFS data and returns issues grouped by severity.
Validation Categories
| Category | Checks |
|---|---|
| Referential Integrity | Orphaned stop_times (missing trip/stop), orphaned trips (missing route), orphaned routes (missing agency) |
| Calendar | Active services exist for today, no expired calendars |
| Coordinates | Valid lat/lon ranges, no zeros, within bounds |
| Transfers | Valid transfer types, referenced stops exist |
| Pathways | Valid pathway types, referenced stops exist |
| Display | Missing route colors, missing route names |
Response
{
"summary": {
"errors": 2,
"warnings": 5,
"infos": 3,
"total_checks": 19
},
"issues": [
{
"severity": "error",
"category": "referential_integrity",
"message": "stop_times reference non-existent trip_id",
"count": 12,
"samples": ["trip_001", "trip_002", "trip_003"]
}
]
}
Each issue includes up to 5 sample IDs for diagnosing the problem. The frontend displays an interactive validation report.
Reload
POST /api/gtfs/reload
Triggers a hot-reload of GTFS data. The server remains fully available during the reload.
Authentication
Requires the API key configured in config.yaml:
curl -X POST http://localhost:8080/api/gtfs/reload \
-H "Authorization: Bearer your-secret-key"
If api_key is empty in the config, this endpoint returns 403 Forbidden.
How It Works
- The request is accepted and returns
200 OKimmediately - A background thread downloads and parses new GTFS data
- A fresh RAPTOR index is built from the new data
- The new index is swapped in atomically via
ArcSwap - In-flight requests continue using the old index until they complete
Use Cases
- Scheduled updates: Call via cron when new GTFS data is published
- CI/CD: Trigger after deploying new data files
- Manual: Reload after editing GTFS files during development
Metrics
GET /api/metrics
Returns server metrics in Prometheus text format.
Available Metrics
Process Metrics
process_cpu_usage— Current CPU usage percentageprocess_memory_bytes— Resident memory in bytesprocess_uptime_seconds— Time since server start
HTTP Metrics
http_requests_total— Total number of HTTP requests receivedhttp_errors_total— Total number of HTTP error responses (4xx, 5xx)
GTFS Metrics
gtfs_agencies— Number of loaded agenciesgtfs_routes— Number of loaded routesgtfs_stops— Number of loaded stopsgtfs_trips— Number of loaded tripsgtfs_stop_times— Number of loaded stop times
Example
curl http://localhost:8080/api/metrics
# HELP process_cpu_usage Current CPU usage
# TYPE process_cpu_usage gauge
process_cpu_usage 12.5
# HELP process_memory_bytes Resident memory
# TYPE process_memory_bytes gauge
process_memory_bytes 524288000
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total 15423
Prometheus Integration
Add Glove to your Prometheus scrape_configs:
scrape_configs:
- job_name: "glove"
scrape_interval: 15s
static_configs:
- targets: ["localhost:8080"]
metrics_path: "/api/metrics"
Frontend Metrics Panel
The frontend includes a built-in metrics dashboard accessible from the sidebar. It displays live values for CPU, memory, uptime, request counts, and GTFS statistics, polling the /api/metrics and /api/status endpoints.
Performance
Benchmarks
The RAPTOR engine is designed for speed: all GTFS data is held in memory with optimized data structures.
Benchmark across 12 origin/destination pairs covering Ile-de-France (10 rounds, single-threaded):

| Metric | Value |
|---|---|
| Min | 215 ms |
| Avg | 371 ms |
| Median | 370 ms |
| p95 | 515 ms |
| Max | 531 ms |
Running Benchmarks
python3 bin/benchmark.py --rounds 10 --concurrency 1
The benchmark script:
- Sends requests to 12 representative origin/destination pairs
- Measures response times across multiple rounds
- Generates a chart with statistics
Key Optimizations
Binary Search in Trip Lookup
The find_earliest_trip function uses binary search (O(log n)) to find the first trip departing after a given time within a pattern, instead of linear scan.
Pre-Allocated Buffers
Label arrays and working buffers are allocated once and reused across RAPTOR rounds, eliminating per-round allocation overhead.
FxHashMap
Uses rustc-hash's FxHashMap throughout both GtfsData and RaptorData, replacing all standard library HashMap instances. FxHash is significantly faster than the default SipHash for integer and string keys.
Lock-Free Hot-Reload
ArcSwap provides atomic pointer swaps with zero contention. Readers never block, even during a reload. There is no mutex, no RwLock, and no read-side overhead.
Early Termination in Diversity Loop
The RAPTOR diversity loop (which re-runs the algorithm with pattern exclusion to find alternative journeys) now terminates early when a round produces no new journeys, avoiding unnecessary iterations.
Arc<WalkLeg> Cache
Walking leg results from Valhalla are wrapped in Arc<WalkLeg> and cached, avoiding deep cloning of polyline coordinates when the same walk leg is referenced by multiple journeys.
Batch Valhalla Calls
Transfer enrichment requests to Valhalla are dispatched in parallel using futures::join_all, rather than sequentially, significantly reducing latency for journeys with multiple transfers.
Pareto-Optimal Exploitation
All Pareto-optimal journeys from a single RAPTOR run are collected before the algorithm is re-run with pattern exclusion. This avoids redundant computation.
Cache Persistence
The RAPTOR index is serialized to disk with a fingerprint derived from the GTFS data. On restart, if the fingerprint matches, the cached index is loaded in sub-second time instead of rebuilding (10-30 seconds).
Pattern Grouping
Trips with identical stop sequences share a single pattern. For a typical Ile-de-France dataset, this reduces the number of entities the algorithm scans by an order of magnitude.
Memory Usage
All GTFS data is held in memory. For the Ile-de-France dataset:
| Data | Approximate Size |
|---|---|
| Stops | ~48,000 entries |
| Trips | ~320,000 entries |
| Stop times | ~8,500,000 entries |
| Patterns | ~15,000 groups |
| Total RAM | ~500 MB |
Monitoring
Health Check
The simplest monitoring is the status endpoint:
curl http://localhost:8080/api/status
This is also used as the Docker healthcheck. A 200 OK response means the server is running and has GTFS data loaded.
Prometheus Metrics
Glove exposes metrics at GET /api/metrics in Prometheus text format. See the Metrics page for details.
Structured Logging
Glove uses the tracing crate for structured logging. Log level is configured in config.yaml:
server:
log_level: "info" # trace, debug, info, warn, error
Override at runtime with the RUST_LOG environment variable:
RUST_LOG=debug cargo run --release
Log Examples
INFO glove::main > Starting Glove on 0.0.0.0:8080
INFO glove::gtfs > Loaded 48000 stops, 320000 trips
INFO glove::raptor > Built RAPTOR index in 12.3s
DEBUG glove::api::journeys > RAPTOR query: 2.3522;48.8566 → 2.2945;48.8584 in 342ms
Rate Limiting
Rate limiting is configured per IP address:
server:
rate_limit: 20 # requests/sec, 0 = disabled
When the limit is exceeded, the server returns 429 Too Many Requests.
Graceful Shutdown
On SIGTERM or SIGINT, Glove:
- Stops accepting new connections
- Waits up to
shutdown_timeoutseconds for in-flight requests to complete - Exits cleanly
server:
shutdown_timeout: 30 # seconds
Development Setup
Prerequisites
Quick Start
# Clone the repository
git clone https://github.com/ltoinel/Glove.git
cd Glove
# Download GTFS data
bin/download.sh gtfs
# Start in dev mode (auto-reload on file changes)
bin/start.sh --dev
Backend Development
cargo build # Debug build
cargo build --release # Release build
cargo test # Run all tests
cargo clippy -- -D warnings # Lint (must pass in CI)
cargo fmt --check # Format check (must pass in CI)
cargo fmt # Auto-format
The dev mode uses cargo-watch to recompile automatically on file changes:
cargo install cargo-watch
cargo watch -x run
Frontend Development
cd portal
npm install # Install dependencies
npm run dev # Vite dev server on port 3000 with HMR
npm run build # Production build to dist/
npx eslint src/ # Lint (must pass in CI)
CI Pipeline
GitHub Actions runs on every push to master and on pull requests:
- Backend: format check, clippy, build, test
- Coverage: cargo-tarpaulin with Codecov upload
- Frontend: ESLint + Vite build
All checks must pass before merging.
Useful Commands
# Run with debug logging
RUST_LOG=debug cargo run
# Run benchmarks
python3 bin/benchmark.py --rounds 10
# Start Valhalla for walk/bike/car routing
bin/valhalla.sh
# Check which GTFS transfer pairs have indoor routing data in Valhalla
python3 bin/check_indoor.py
The check_indoor.py script queries Valhalla for each GTFS transfer pair to determine which ones have indoor routing data available from OSM. This is useful for understanding indoor coverage in your deployment area.
Project Structure
Glove/
├── src/ # Rust backend
│ ├── main.rs # Entry point, server setup, metrics middleware
│ ├── config.rs # Nested YAML configuration
│ ├── gtfs.rs # GTFS data model & CSV loader
│ ├── raptor.rs # RAPTOR algorithm & index building
│ ├── ban.rs # BAN address geocoding
│ ├── text.rs # Text normalization (diacritics)
│ └── api/
│ ├── mod.rs # Shared response types
│ ├── journeys/
│ │ ├── mod.rs # Journey module entry
│ │ ├── public_transport.rs # RAPTOR journey planning
│ │ ├── walk.rs # Walking via Valhalla
│ │ ├── bike.rs # Cycling (3 profiles) via Valhalla
│ │ ├── car.rs # Driving via Valhalla
│ │ └── valhalla.rs # Shared Valhalla HTTP client
│ ├── places.rs # Autocomplete (stops + addresses)
│ ├── gtfs.rs # GTFS validation & reload endpoints
│ ├── tiles.rs # Map tile proxy with disk cache
│ ├── metrics.rs # Prometheus metrics endpoint
│ └── status.rs # Status endpoint
│
├── portal/ # React frontend
│ ├── src/
│ │ ├── App.jsx # Main SPA (search, results, map, metrics)
│ │ ├── i18n.jsx # Internationalization (FR/EN)
│ │ ├── main.jsx # Entry point with MUI theme
│ │ ├── index.css # Styling
│ │ ├── utils.js # Pure utility functions (tested with vitest)
│ │ └── test/ # Vitest test files
│ ├── package.json
│ ├── vite.config.js
│ └── eslint.config.js
│
├── bin/ # Utility scripts
│ ├── start.sh # Start script (production & dev)
│ ├── download.sh # Data download (GTFS, OSM, BAN)
│ └── valhalla.sh # Valhalla Docker setup
│
├── scripts/ # Analysis & benchmarking
│ ├── benchmark.py # Performance benchmark with charts
│ └── check_indoor.py # Check GTFS transfers for indoor routing data
│
├── docker/
│ └── Dockerfile # Multi-stage build (Node + Rust + Debian)
│
├── book/ # Documentation (mdBook)
│ ├── book.toml
│ └── src/
│ └── images/ # Documentation images (screenshots, benchmarks)
│
├── data/ # Data files (not committed)
│ ├── gtfs/ # GTFS transit schedules
│ ├── osm/ # OpenStreetMap data
│ ├── raptor/ # Serialized RAPTOR index cache
│ ├── ban/ # French address data
│ ├── tiles/ # Cached map tiles (auto-populated)
│ └── valhalla/ # Valhalla routing tiles
│
├── config.yaml # Application configuration
├── Cargo.toml # Rust dependencies
├── CLAUDE.md # AI assistant guidance
├── README.md # Project overview
├── LICENSE.md # MIT license
└── .github/workflows/ci.yml # CI pipeline
Key Files
| File | Lines | Description |
|---|---|---|
src/raptor.rs | ~2,200 | RAPTOR algorithm, the core of the application |
portal/src/App.jsx | ~2,200 | Entire frontend SPA in one file |
src/api/journeys/public_transport.rs | ~1,450 | Journey planning endpoint and response formatting |
src/api/gtfs.rs | ~830 | GTFS validation (19 checks) & reload endpoint |
src/config.rs | ~750 | Configuration with defaults (server, routing, map, bike, wheelchair) |
src/ban.rs | ~630 | BAN address geocoding with number interpolation |
src/gtfs.rs | ~570 | GTFS CSV parsing and data model |
src/api/places.rs | ~340 | Fuzzy search with ranking |
src/api/metrics.rs | ~370 | Prometheus metrics collection |
src/api/tiles.rs | ~110 | Map tile proxy with disk cache |
GTFS Data Overview
Glove loads transit data in the GTFS format (General Transit Feed Specification), an open standard used worldwide by transit agencies to describe their schedules, stops, and routes. The data is published by Ile-de-France Mobilités (IDFM), the public transit authority for the Paris region.
What is GTFS?
GTFS is a collection of CSV files that together describe a complete transit network. Think of it as a structured way to answer: "What vehicles go where, at what time, and how do passengers connect between them?"
All GTFS data is loaded into memory at startup. There is no database — the entire transit network lives in RAM for maximum query speed. This is what makes Glove fast: a journey query scans in-memory data structures instead of hitting a disk.
Dataset at a Glance
| File | Records | What it contains |
|---|---|---|
| agencies | 61 | Transit operators (RATP, SNCF, local bus companies...) |
| routes | 2,009 | Transit lines — each bus line, metro line, or RER line is a route |
| stops | 54,011 | Physical locations where passengers board or alight |
| trips | 495,345 | Individual vehicle runs — one bus doing its morning route is one trip |
| stop_times | 10,933,796 | The schedule: what time each trip arrives/departs at each stop |
| transfers | 206,822 | Walking connections between nearby stops (for changing lines) |
| calendars | 1,169 | Service patterns: which days each schedule runs (weekdays, weekends...) |
| calendar_dates | 2,937 | Exceptions to the calendar (holidays, strikes, special events...) |
Understanding the scale
To put these numbers in perspective:
- 10.9 million stop times means the dataset contains nearly 11 million individual "a vehicle stops here at this time" records. This is the bulk of the data.
- 495,345 trips represent every individual vehicle departure across all lines, all day, all week. A single metro line might have hundreds of trips per day.
- 206,822 transfers define where passengers can walk between stops to change lines. For example, walking from a metro platform to a nearby bus stop.
Understanding GTFS Objects
Agencies
An agency is a transit operator. Ile-de-France has 61 agencies, from large operators like RATP (Paris metro, buses, tramways) to small local bus companies covering specific towns.
Routes
A route is a transit line as passengers know it — "Metro line 4", "Bus 72", or "RER A". Each route has:
- A short name displayed on vehicles and maps (e.g. "4", "72", "A")
- A type indicating the transport mode (metro, bus, rail, tramway...)
- A color for visual identification
Stops
A stop is a physical place where passengers board or leave a vehicle. GTFS has three levels:
- Stop points (36,126) — the actual boarding location, like a specific platform or bus bay. "RER A, platform 1, Gare de Lyon" is a stop point.
- Stations (15,369) — a group of stop points. "Gare de Lyon" is a station that contains stop points for RER A, RER D, metro 1, metro 14, and several bus lines.
- Entrances (2,516) — physical entry/exit points to a station, like a specific stairway or elevator from street level.
Trips
A trip is one vehicle making one run along a route. For example, "the metro line 4 departing Porte de Clignancourt at 08:15" is a trip. The same route has hundreds of trips per day, one for each departure.
Stop Times
A stop time is the scheduled arrival and departure of a trip at a specific stop. It's the core of the timetable: "trip T123 arrives at Châtelet at 08:22 and departs at 08:23". With nearly 11 million of these, the dataset covers every scheduled stop of every vehicle.
Transfers
A transfer defines a walking connection between two stops, with a minimum transfer time in seconds. This tells the routing algorithm: "a passenger can walk from stop A to stop B in 120 seconds to change lines." Without transfers, the algorithm wouldn't know that two stops are close enough to walk between.
Calendars & Calendar Dates
Calendars define weekly patterns: "this schedule runs Monday to Friday" or "weekends and holidays only." Calendar dates are exceptions: "this schedule does NOT run on December 25" or "this special schedule runs on July 14." Together, they let Glove know which trips are active on any given date.
Routes by Transport Mode
| Mode | GTFS Type | Routes | Description |
|---|---|---|---|
| Bus | 3 | 1,950 | Urban and suburban bus lines |
| Rail | 2 | 24 | RER, Transilien, and TER regional trains |
| Tramway | 0 | 17 | Tramway lines and automated shuttles |
| Métro | 1 | 16 | Paris underground metro |
| Funiculaire | 7 | 1 | Montmartre funicular |
| Navette | 6 | 1 | Automated shuttle |
Bus routes represent 97% of all routes by count, but metro and RER carry the majority of daily passengers. Glove routes across all modes.
Métro Lines
16 metro lines operated by RATP, forming the backbone of urban transit in Paris:
| Line | Line | Line | Line | |||
|---|---|---|---|---|---|---|
| 1 | 5 | 9 | 13 | |||
| 2 | 6 | 10 | 14 | |||
| 3 | 7 | 11 | 3B | |||
| 4 | 8 | 12 | 7B |
Lines 3B and 7B are short branch lines. Line 14 is fully automated and the newest, recently extended to Orly airport.
RER Lines
5 RER (Réseau Express Régional) lines cross Paris and connect the city center to the suburbs and airports:
| Line | Terminals |
|---|---|
| A | Saint-Germain / Cergy / Poissy — Marne-la-Vallée / Boissy |
| B | CDG Airport / Mitry — Robinson / Saint-Rémy |
| C | Versailles / Saint-Quentin — Dourdan / Saint-Martin-d'Étampes |
| D | Orry-la-Ville / Creil — Melun / Malesherbes / Corbeil |
| E | Haussmann — Chelles / Tournan / Mantes (via Nanterre) |
RER lines are heavier, faster, and cover longer distances than the metro. They are the primary way to reach airports (CDG via RER B, Orly via RER C) and major suburban hubs.
Transilien Lines
9 suburban rail lines operated by SNCF, serving the outer suburbs:
| Line | Line | Line | ||
|---|---|---|---|---|
| H | L | R | ||
| J | N | U | ||
| K | P | V |
Transilien lines depart from the major Paris train stations (Saint-Lazare, Gare du Nord, Gare de Lyon, Montparnasse, Gare de l'Est) and serve towns beyond the reach of metro and RER.
TER Lines
Regional trains (Train Express Régional) from neighboring regions that serve stations within Ile-de-France:
- TER Bourgogne - Franche-Comté
- TER Centre - Val de Loire
- TER Hauts-de-France
- TER Normandie
- TER Grand-Est
These are operated by SNCF for the respective regions and provide connections to cities outside Ile-de-France.
Tramway Lines
17 tramway and automated lines, mostly running in the inner suburbs:
| Line | Line | Line | ||
|---|---|---|---|---|
| T1 | T6 | T11 | ||
| T2 | T7 | T12 | ||
| T3a | T8 | T13 | ||
| T3b | T9 | T14 | ||
| T4 | T10 | CDG VAL | ||
| T5 | ORLYVAL |
T3a and T3b form a ring around Paris along the boulevards des Maréchaux. CDG VAL and ORLYVAL are automated airport shuttles.
Stops Hierarchy
GTFS organizes stops in a three-level hierarchy:
| Level | Count | What it represents |
|---|---|---|
| Stop points | 36,126 | The exact spot where you board — a platform, a bus bay, a specific door |
| Stations | 15,369 | A named place grouping multiple stop points — "Gare du Nord" contains platforms for metro 4, metro 5, RER B, RER D, RER E, Transilien H, and bus stops |
| Entrances | 2,516 | Physical ways into a station — a stairway, an elevator, a specific street-level door |
This hierarchy is important for routing: when you search for "Gare du Nord", Glove finds the station and then considers all its stop points to find the best boarding platform for your journey.
Top 15 Transit Operators
| Operator | Routes | Coverage |
|---|---|---|
| RATP | 246 | Metro, RER A/B, tramways, Paris buses |
| Centre et Sud Yvelines | 109 | Bus network in southern Yvelines |
| Poissy - Les Mureaux | 88 | Bus network in northern Yvelines |
| Coeur d'Essonne | 66 | Bus network in central Essonne |
| Mantois | 64 | Bus network around Mantes-la-Jolie |
| Brie et 2 Morin | 64 | Bus network in eastern Seine-et-Marne |
| Roissy Ouest | 63 | Bus network near CDG airport |
| Meaux et Ourcq | 55 | Bus network around Meaux |
| Pays Briard | 55 | Bus network in southern Seine-et-Marne |
| Paris Saclay | 53 | Bus network around the Saclay plateau |
| Argenteuil - Boucles de Seine | 53 | Bus network in northern Hauts-de-Seine |
| Saint-Quentin-en-Yvelines | 51 | Bus network around SQY |
| Provinois - Brie et Seine | 48 | Bus network in far eastern Seine-et-Marne |
| Fontainebleau - Moret | 47 | Bus network around Fontainebleau |
| Essonne Sud Ouest | 47 | Bus network in southwestern Essonne |
RATP is by far the largest operator, running all metro lines, RER A and B, most tramway lines, and a massive bus network covering Paris and the near suburbs. The remaining 60 operators are organized by geographic zone and operate bus-only networks.
Data Source
The GTFS dataset is downloaded from:
https://data.iledefrance-mobilites.fr/explore/dataset/offre-horaires-tc-gtfs-idfm/
It is updated regularly by IDFM (typically every few weeks when schedules change). Glove can hot-reload the data via POST /api/gtfs/reload without service interruption — the new RAPTOR index is built in a background thread and swapped in atomically.
Routing Statistics
This page presents routing statistics for the IDFM GTFS dataset processed by Glove.
RAPTOR Index
After loading and pre-processing the GTFS data, Glove builds a RAPTOR index with the following characteristics:
| Metric | Value |
|---|---|
| Stops indexed | 54,011 |
| Trips loaded | 495,345 |
| Stop times | 10,933,796 |
| Patterns (grouped trips) | ~15,000 |
| Transfer pairs | 206,822 |
| Index build time | 10-30 seconds |
| Index cache size | ~200 MB |
| RAM usage (loaded) | ~500 MB |
Trips with identical stop sequences are grouped into patterns. For the IDFM dataset, ~495,000 trips are reduced to ~15,000 patterns — a 33x reduction that directly speeds up the RAPTOR scan phase.
Query Performance
Benchmark across 12 representative origin/destination pairs covering Ile-de-France (10 rounds, single-threaded):

| Metric | Value |
|---|---|
| Min | 215 ms |
| Average | 371 ms |
| Median | 370 ms |
| p95 | 515 ms |
| Max | 531 ms |
Run the benchmark:
python3 bin/benchmark.py --rounds 10 --concurrency 1
Indoor Routing Coverage
Valhalla pedestrian routing enriches transfer sections with indoor maneuvers when OSM data is available.
| Metric | Value |
|---|---|
| Transfer pairs analyzed | 71,479 |
| With indoor data | 3,729 (5.2%) |
| Outdoor only | 67,745 (94.8%) |
| Stations with indoor data | 355 / 9,047 (3.9%) |
Indoor Maneuvers Found
| Type | Count |
|---|---|
| Escalator | 3,060 |
| Stairs | 1,441 |
| Elevator | 1,296 |
| Enter building | 34 |
| Exit building | 26 |
Top 10 Stations by Indoor Score
| Station | Score | Indoor Ratio |
|---|---|---|
| Gare Saint-Lazare | 1,344 | 53% |
| La Défense | 1,089 | 57% |
| Gare du Nord | 646 | 52% |
| Massy - Palaiseau | 633 | 87% |
| Gare Montparnasse | 527 | 27% |
| Versailles Chantiers | 478 | 77% |
| Opéra | 384 | 34% |
| République | 332 | 32% |
| Juvisy | 304 | 74% |
| Gare de l'Est | 260 | 32% |
For the full analysis, see Indoor Routing Coverage.
Valhalla Routing Modes
In addition to public transit (RAPTOR), Glove provides walk/bike/car routing via Valhalla with OSM data for Ile-de-France.
| Mode | Costing | Key Options |
|---|---|---|
| Walk | pedestrian | step_penalty: 30, elevator_penalty: 60 |
| City bike | bicycle | 16 km/h, avoid hills and roads |
| E-bike | bicycle | 21 km/h, hills easy with motor |
| Road bike | bicycle | 25 km/h, prefer smooth tarmac |
| Car | auto | Default Valhalla settings |
Bike Profiles
Three bike profiles are configured for the Ile-de-France context:
| Profile | Speed | Use Case | Hills | Roads |
|---|---|---|---|---|
| City (Vélib') | 16 km/h | Dense urban areas | Avoid (0.3) | Avoid (0.2) |
| E-bike (VAE) | 21 km/h | Commuting | Easy (0.8) | Accept (0.4) |
| Road | 25 km/h | Fast commuters | Moderate (0.5) | Prefer (0.6) |
Coverage Area
The default configuration covers the 8 departments of Ile-de-France:
| Department | Code |
|---|---|
| Paris | 75 |
| Seine-et-Marne | 77 |
| Yvelines | 78 |
| Essonne | 91 |
| Hauts-de-Seine | 92 |
| Seine-Saint-Denis | 93 |
| Val-de-Marne | 94 |
| Val-d'Oise | 95 |
Map bounds: SW (48.1, 1.4) to NE (49.3, 3.6), centered on Paris (48.8566, 2.3522).
Indoor Routing Coverage
This report analyses the availability of indoor pedestrian routing data in Valhalla tiles for GTFS transfer pairs in Ile-de-France. Indoor maneuvers (elevators, stairs, escalators, building entrances/exits) enable accurate turn-by-turn instructions for station transfers.
The bin/check_indoor.py script queries Valhalla's pedestrian route API for each unique GTFS transfer pair and checks whether the response contains indoor maneuver types (39-43). See Development Setup for usage.
Summary
| Metric | Value |
|---|---|
| Total transfer pairs checked | 71,479 |
| With indoor routing data | 3,729 (5.2%) |
| Outdoor only | 67,745 (94.8%) |
| Routing errors | 5 |
| Stations with indoor data | 355 / 9,047 (3.9%) |
Indoor Maneuver Types
| Type | Count | Description |
|---|---|---|
| Escalator | 3,060 | Escalator transitions between levels |
| Stairs | 1,441 | Staircase transitions |
| Elevator | 1,296 | Elevator/lift transitions |
| Enter building | 34 | Entering a station building |
| Exit building | 26 | Exiting a station building |
Escalators are the most commonly mapped indoor element, followed by stairs and elevators. Building entrance/exit transitions are rare, found mainly at CDG airport terminals.
Top 25 Stations by Indoor Coverage
Stations ranked by indoor score (total count of indoor maneuvers across all transfer pairs involving the station).
| Station | Score | Ratio | Elevators | Stairs | Escalators |
|---|---|---|---|---|---|
| Gare Saint-Lazare | 1,344 | 53% | 85 | 324 | 935 |
| La Défense | 1,089 | 57% | 47 | 0 | 1,042 |
| Gare du Nord | 646 | 52% | 8 | 313 | 325 |
| Massy - Palaiseau | 633 | 87% | 229 | 0 | 404 |
| Gare Montparnasse | 527 | 27% | 0 | 48 | 479 |
| Versailles Chantiers | 478 | 77% | 140 | 0 | 338 |
| Opera | 384 | 34% | 0 | 228 | 156 |
| Republique | 332 | 32% | 0 | 288 | 44 |
| Juvisy | 304 | 74% | 242 | 0 | 62 |
| Gare de l'Est | 260 | 32% | 8 | 88 | 164 |
| Saint-Quentin en Yvelines | 243 | 61% | 145 | 0 | 98 |
| Gare de Lyon | 226 | 25% | 10 | 65 | 151 |
| Europe | 210 | 32% | 0 | 134 | 76 |
| Corbeil-Essonnes | 206 | 58% | 8 | 198 | 0 |
| Bibliotheque Francois Mitterrand | 198 | 50% | 29 | 3 | 166 |
| Aeroport CDG Terminal 2 (TGV) | 180 | 68% | 36 | 0 | 66 |
| Fontainebleau - Avon | 164 | 74% | 137 | 27 | 0 |
| Aulnay-sous-Bois | 140 | 59% | 140 | 0 | 0 |
| Neuilly - Porte Maillot | 140 | 18% | 0 | 32 | 108 |
| Magenta | 126 | 75% | 4 | 13 | 109 |
| Haussmann Saint-Lazare | 119 | 100% | 37 | 16 | 66 |
| Evry - Courcouronnes | 87 | 28% | 21 | 0 | 66 |
| Havre - Caumartin | 81 | 10% | 16 | 21 | 44 |
| Bondy | 75 | 59% | 8 | 67 | 0 |
| Rosny-sous-Bois | 64 | 31% | 0 | 64 | 0 |
Key Observations
Well-covered stations
The major railway hubs have the best indoor coverage:
- Gare Saint-Lazare has the highest indoor score (1,344) with a good mix of escalators, stairs, and elevators. Over half of its transfer pairs include indoor maneuvers.
- La Defense scores second (1,089), dominated by escalators — consistent with its large underground hub connecting RER, metro, and tramway.
- Gare du Nord and Gare Montparnasse are well mapped, with escalators and stairs.
- Haussmann Saint-Lazare achieves a 100% indoor ratio — all its transfer pairs return indoor maneuvers.
RER/Transilien stations
Several suburban stations have high indoor ratios thanks to elevator mappings:
- Massy-Palaiseau (87%), Versailles Chantiers (77%), Juvisy (74%), Fontainebleau-Avon (74%) — these stations likely have well-mapped accessibility elevators in OSM.
- Aulnay-sous-Bois has 140 elevator maneuvers, suggesting comprehensive accessibility mapping.
Metro stations
Metro stations have lower coverage. Opera (34%), Republique (32%), and Europe (32%) appear in the top 25 but with moderate ratios. Most metro stations have no indoor data — the corridors and stairs connecting platforms are not yet mapped in OSM.
Gaps
- 94.8% of transfer pairs have no indoor routing data
- Only 3.9% of stations (355/9,047) have any indoor coverage
- Building enter/exit maneuvers are almost exclusively at CDG airport
- Most central Paris metro interchange stations (Chatelet, Nation, Strasbourg Saint-Denis...) are missing from the indoor data
How Glove Uses This Data
Glove only displays transfer maneuvers when Valhalla returns indoor routing data. For the 94.8% of transfers without indoor data, the transfer section shows only the duration and stop names — no potentially misleading outdoor walking route is displayed.
This ensures that:
- Users at Gare Saint-Lazare see: "Take the escalator to Level 2"
- Users at Chatelet see only: "Transfer 3 min" (no false outdoor route)
Improving Coverage
Indoor coverage depends on OSM contributors mapping station interiors. Key tags to add:
| OSM Tag | Description |
|---|---|
highway=footway | Indoor walkways/corridors |
highway=steps | Staircases between levels |
highway=elevator | Elevators/lifts |
indoor=yes | Marks a way as indoor |
level=* | Floor level (-1, 0, 1, ...) |
railway=subway_entrance | Metro station entrance nodes |
Tools for editing indoor data:
- OpenLevelUp — visualize existing indoor data by level
- JOSM with IndoorHelper plugin — edit indoor features
- Overpass Turbo — query indoor tags around a station