Glove

CI codecov License: MIT

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

Glove screenshot

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

Quick Start

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

Requirements

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)

Note

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-watch for 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

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
SettingDescriptionDefault
bindNetwork interface to listen on0.0.0.0
portHTTP port8080
workersActix worker threads. 0 = one per CPU core1
log_levelMinimum log levelinfo
shutdown_timeoutSeconds to wait for in-flight requests on shutdown30
api_keyAPI key for the reload endpoint. Empty disables the endpoint""
cors_originsList of allowed CORS origins. ["*"] allows all[]
rate_limitMaximum requests per second per IP address20

Tip

Override the log level at runtime with RUST_LOG=debug cargo run.

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]
SettingDescription
dirBase data directory. Sub-directories gtfs/, osm/, raptor/, ban/ are created automatically
gtfs_urlURL to download the GTFS zip archive
osm_urlURL to download the OpenStreetMap PBF file (for Valhalla)
ban_urlBase URL for BAN address CSV files
departmentsFrench 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)
SettingDescriptionDefault
max_journeysMaximum number of alternative journeys to return5
max_transfersMaximum number of transfers in a journey5
default_transfer_timeDefault walking time between stops (seconds)120
max_durationMaximum total journey duration (seconds)10800 (3h)
max_nearest_stop_distanceMaximum 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.

SettingDescriptionDefault
zoomDefault map zoom level11
center_lat / center_lonDefault map center48.8566 / 2.3522 (Paris)
bounds_sw_* / bounds_ne_*Geographic bounds (SW and NE corners)Île-de-France
tile_urlUpstream tile server URL template. Placeholders: {s} (subdomain), {z}, {x}, {y}, {r} (retina)CARTO Voyager
tile_cache_durationBrowser 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:

ProfileSpeedUse Case
City16 km/hVelib' / city bikes, avoids hills and busy roads
E-bike21 km/hElectric bikes (VAE), handles hills easily
Road25 km/hRoad 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.

SettingDescriptionDefault
step_penaltyPenalty for stairs. Very high value effectively avoids them999999
max_gradeMaximum road grade in percent (6% is the standard wheelchair norm)6
use_hillsHill avoidance factor (0.0 = strongly avoid, 1.0 = no preference)0.0
elevator_penaltyPenalty for elevators (0 = prefer them)0
walking_speedWheelchair speed in km/h3.5

Info

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:

  1. Node.js (node:20-alpine) — builds the React frontend with Vite
  2. Rust (rust:1.87) — compiles the backend in release mode
  3. 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.yaml mounted 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:

  1. Pulls the ghcr.io/valhalla/valhalla Docker image
  2. Builds routing tiles from the downloaded OSM data
  3. Starts the container on port 8002

The Valhalla configuration includes:

  • include_platforms=True to import platform/indoor data from OSM
  • step_penalty and elevator_penalty in 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

Warning

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

Frontend (React) Leaflet Map · MUI Sidebar · i18n HTTP / JSON Actix-web Server CORS · Rate Limiting · Metrics Middleware /journeys /walk /bike /car /places /status /gtfs/* /metrics /tiles cache proxy RAPTOR Index ArcSwap · Lock-free BAN Index Addresses GTFS Data CSV files Valhalla Docker · Walk / Bike / Car

Design Principles

All In-Memory

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

Lock-Free Hot-Reload

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.

Pattern Grouping

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.

Indoor Routing

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.

Navitia API Compatibility

The API mirrors Navitia query parameters and response structure, making Glove a potential drop-in replacement for Navitia-based applications.

Technology Stack

ComponentTechnology
BackendRust, Actix-web 4
RoutingRAPTOR algorithm (custom implementation)
Walk/Bike/CarValhalla (Docker, with indoor routing support)
FrontendReact 19, Vite, MUI 7, Leaflet
Data formatGTFS (General Transit Feed Specification)
Address searchBAN (Base Adresse Nationale)
Serializationserde (JSON + YAML + CSV)
API docsutoipa (OpenAPI auto-generation)
MonitoringCustom Prometheus metrics
Loggingtracing + 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:

  1. Collects marked stops — stops that were improved in the previous round
  2. 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
  3. 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:

IndexPurpose
Stop spatial indexMaps coordinates to nearby stops within max_nearest_stop_distance
Service ID interningConverts string service IDs to integers for fast calendar lookups
Pattern groupingGroups trips with identical stop sequences into patterns
Transfer graphPrecomputes walking transfers between nearby stops
Calendar indexMaps 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:

  1. Run RAPTOR and collect all Pareto-optimal journeys from the result
  2. For each journey found, record which patterns were used
  3. Run RAPTOR again, excluding previously used patterns
  4. Repeat until max_journeys is 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.txt are applied (additions and removals)
  • Only trips belonging to active services are considered during the scan

The autocomplete endpoint uses a ranked fuzzy search with French diacritics normalization:

  1. Exact match (highest priority)
  2. Prefix match (stop name starts with query)
  3. Word-prefix match (any word in the stop name starts with query)
  4. 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_trip for 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

config.yaml Load Config → Check Cache Cache valid? YES Load cache sub-second NO Parse GTFS Build RAPTOR 10-30 seconds Load BAN data addresses Start Actix-web Serve API + SPA

GTFS Data Model

Glove loads the following GTFS files:

FileContentRust Struct
agency.txtTransit agenciesAgency
routes.txtTransit routes (lines)Route
stops.txtStop locationsStop
trips.txtIndividual tripsTrip
stop_times.txtArrival/departure at each stopStopTime
calendar.txtWeekly service schedulesCalendar
calendar_dates.txtService exceptionsCalendarDate
transfers.txtTransfer connections between stopsTransfer

Query Flow

Public Transit Journey

Client Request Parse query parameters from, to, datetime Find nearest stops origin and destination Run RAPTOR Collect Pareto-optimal journeys Enough? NO exclude patterns YES Reconstruct journeys Tag · Format Navitia response JSON Response

Walk / Bike / Car Journey

Client Request Build Valhalla request costing model + options Call Valhalla /route HTTP on localhost:8002 Decode polyline · Extract maneuvers Elevation colors (bike) · Format Navitia response JSON Response

Hot Reload

The hot reload mechanism allows updating GTFS data without downtime:

  1. POST /api/gtfs/reload is called (requires api_key)
  2. A background thread loads new GTFS data and builds a fresh RAPTOR index
  3. The new index is swapped in atomically via ArcSwap
  4. All in-flight requests continue using the old index until they complete
  5. 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

LibraryVersionPurpose
React19UI framework
Vite-Build tool with HMR
MUI (Material-UI)7Component 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_accessible journey tag is displayed

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

MethodPathDescription
GET/api/journeys/public_transportPublic transit journey planning (RAPTOR)
GET/api/journeys/walkWalking directions (Valhalla)
GET/api/journeys/bikeCycling directions (Valhalla, 3 profiles)
GET/api/journeys/carDriving directions (Valhalla)
GET/api/placesStop and address autocomplete
GET/api/statusGTFS stats and server status
GET/api/gtfs/validateGTFS data quality validation (19 checks)
POST/api/gtfs/reloadHot-reload GTFS data
GET/api/metricsPrometheus-format metrics
GET/api/tiles/{z}/{x}/{y}.pngMap tile proxy with local disk cache
GET/api-docs/openapi.jsonOpenAPI specification

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"

Warning

If api_key is empty in the config, the reload and validate endpoints are disabled.

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

ParameterTypeRequiredDescription
fromstringYesOrigin coordinates (lon;lat)
tostringYesDestination coordinates (lon;lat)
datetimestringNoDeparture time (ISO 8601, e.g. 20240315T083000). Defaults to now
datetime_representsstringNoWhether datetime is departure (default) or arrival
maneuversboolNoInclude turn-by-turn maneuvers in response (default: false). When absent, maneuvers are omitted and transfer Valhalla enrichment is skipped
wheelchairboolNoEnable wheelchair-accessible routing (default: false). Avoids stairs, limits slope, prefers elevators. Adds most_accessible journey tag
forbidden_modesstringNoComma-separated commercial modes to exclude (e.g. metro,bus,rail)
forbidden_uris[]stringNoRoute IDs to exclude from routing
walking_speedfloatNoWalking speed override in m/s (default: ~1.12 m/s = 4 km/h)
max_nb_transfersintNoMaximum number of transfers allowed
min_nb_transfersintNoMinimum number of transfers
max_durationintNoMaximum journey duration in seconds
max_walking_duration_to_ptintNoMaximum walking time to reach transit (seconds)
first_section_mode[]stringNoModes allowed for the first leg (e.g. walking, bike, car)
last_section_mode[]stringNoModes allowed for the last leg
direct_pathstringNoInclude direct non-transit path (none, only)
countintNoNumber of journeys requested
max_nb_journeysintNoMaximum 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 duration
  • least_transfers — Fewest number of transfers
  • least_walking — Least total walking time, including both street_network sections (first/last mile) and transfer durations
  • most_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:

FieldDescription
instructionHuman-readable direction text
maneuver_typeValhalla 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).

Maneuver Types

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.

ParameterTypeRequiredDescription
fromstringYesOrigin (lon;lat)
tostringYesDestination (lon;lat)
maneuversboolNoInclude turn-by-turn maneuvers (default: false)
wheelchairboolNoWheelchair-accessible routing: avoids stairs, limits slope to 6%, speed 3.5 km/h

Cycling

GET /api/journeys/bike

Uses Valhalla with configurable bike profiles.

ParameterTypeRequiredDescription
fromstringYesOrigin (lon;lat)
tostringYesDestination (lon;lat)
profilestringNoBike profile: city, ebike, or road (default: city)
maneuversboolNoInclude turn-by-turn maneuvers (default: false)

Elevation Colors

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.

ParameterTypeRequiredDescription
fromstringYesOrigin (lon;lat)
tostringYesDestination (lon;lat)
maneuversboolNoInclude 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.

Tip

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.

ParameterTypeDescription
zintegerZoom level (0–20)
xintegerTile column
yintegerTile 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).

Rate Limiting

Tile requests are excluded from the per-IP rate limiting to allow smooth map panning.

Places & Autocomplete

GET /api/places

Search for transit stops and addresses with fuzzy matching.

Parameters

ParameterTypeRequiredDescription
qstringYesSearch query (e.g. "gare de lyon")
limitintegerNoMaximum 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:

  1. Exact match — "Châtelet" matches "Châtelet" (highest priority)
  2. Prefix match — "chat" matches "Châtelet"
  3. Word-prefix match — "lyon" matches "Gare de Lyon"
  4. 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:

  • chatelet matches Châtelet
  • gare de l'est matches Gare de l'Est
  • opera matches Opé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

CategoryChecks
Referential IntegrityOrphaned stop_times (missing trip/stop), orphaned trips (missing route), orphaned routes (missing agency)
CalendarActive services exist for today, no expired calendars
CoordinatesValid lat/lon ranges, no zeros, within bounds
TransfersValid transfer types, referenced stops exist
PathwaysValid pathway types, referenced stops exist
DisplayMissing 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

  1. The request is accepted and returns 200 OK immediately
  2. A background thread downloads and parses new GTFS data
  3. A fresh RAPTOR index is built from the new data
  4. The new index is swapped in atomically via ArcSwap
  5. 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 percentage
  • process_memory_bytes — Resident memory in bytes
  • process_uptime_seconds — Time since server start

HTTP Metrics

  • http_requests_total — Total number of HTTP requests received
  • http_errors_total — Total number of HTTP error responses (4xx, 5xx)

GTFS Metrics

  • gtfs_agencies — Number of loaded agencies
  • gtfs_routes — Number of loaded routes
  • gtfs_stops — Number of loaded stops
  • gtfs_trips — Number of loaded trips
  • gtfs_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):

Benchmark

MetricValue
Min215 ms
Avg371 ms
Median370 ms
p95515 ms
Max531 ms

Running Benchmarks

python3 bin/benchmark.py --rounds 10 --concurrency 1

The benchmark script:

  1. Sends requests to 12 representative origin/destination pairs
  2. Measures response times across multiple rounds
  3. 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:

DataApproximate 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:

  1. Stops accepting new connections
  2. Waits up to shutdown_timeout seconds for in-flight requests to complete
  3. Exits cleanly
server:
  shutdown_timeout: 30    # seconds

Development Setup

Prerequisites

  • Rust 1.85+ (with cargo-watch for dev mode)
  • Node.js 18+ with npm
  • Docker (optional, for Valhalla)

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:

  1. Backend: format check, clippy, build, test
  2. Coverage: cargo-tarpaulin with Codecov upload
  3. 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

Indoor Coverage Analysis

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

FileLinesDescription
src/raptor.rs~2,200RAPTOR algorithm, the core of the application
portal/src/App.jsx~2,200Entire frontend SPA in one file
src/api/journeys/public_transport.rs~1,450Journey planning endpoint and response formatting
src/api/gtfs.rs~830GTFS validation (19 checks) & reload endpoint
src/config.rs~750Configuration with defaults (server, routing, map, bike, wheelchair)
src/ban.rs~630BAN address geocoding with number interpolation
src/gtfs.rs~570GTFS CSV parsing and data model
src/api/places.rs~340Fuzzy search with ranking
src/api/metrics.rs~370Prometheus metrics collection
src/api/tiles.rs~110Map 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?"

How Glove uses GTFS

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

FileRecordsWhat it contains
agencies61Transit operators (RATP, SNCF, local bus companies...)
routes2,009Transit lines — each bus line, metro line, or RER line is a route
stops54,011Physical locations where passengers board or alight
trips495,345Individual vehicle runs — one bus doing its morning route is one trip
stop_times10,933,796The schedule: what time each trip arrives/departs at each stop
transfers206,822Walking connections between nearby stops (for changing lines)
calendars1,169Service patterns: which days each schedule runs (weekdays, weekends...)
calendar_dates2,937Exceptions 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

ModeGTFS TypeRoutesDescription
Bus31,950Urban and suburban bus lines
Rail224RER, Transilien, and TER regional trains
Tramway017Tramway lines and automated shuttles
Métro116Paris underground metro
Funiculaire71Montmartre funicular
Navette61Automated 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:

LineLineLineLine
15913
261014
37113B
48127B

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:

LineTerminals
ASaint-Germain / Cergy / Poissy — Marne-la-Vallée / Boissy
BCDG Airport / Mitry — Robinson / Saint-Rémy
CVersailles / Saint-Quentin — Dourdan / Saint-Martin-d'Étampes
DOrry-la-Ville / Creil — Melun / Malesherbes / Corbeil
EHaussmann — 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:

LineLineLine
HLR
JNU
KPV

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:

LineLineLine
T1T6T11
T2T7T12
T3aT8T13
T3bT9T14
T4T10CDG VAL
T5ORLYVAL

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:

LevelCountWhat it represents
Stop points36,126The exact spot where you board — a platform, a bus bay, a specific door
Stations15,369A 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
Entrances2,516Physical 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

OperatorRoutesCoverage
RATP246Metro, RER A/B, tramways, Paris buses
Centre et Sud Yvelines109Bus network in southern Yvelines
Poissy - Les Mureaux88Bus network in northern Yvelines
Coeur d'Essonne66Bus network in central Essonne
Mantois64Bus network around Mantes-la-Jolie
Brie et 2 Morin64Bus network in eastern Seine-et-Marne
Roissy Ouest63Bus network near CDG airport
Meaux et Ourcq55Bus network around Meaux
Pays Briard55Bus network in southern Seine-et-Marne
Paris Saclay53Bus network around the Saclay plateau
Argenteuil - Boucles de Seine53Bus network in northern Hauts-de-Seine
Saint-Quentin-en-Yvelines51Bus network around SQY
Provinois - Brie et Seine48Bus network in far eastern Seine-et-Marne
Fontainebleau - Moret47Bus network around Fontainebleau
Essonne Sud Ouest47Bus 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:

MetricValue
Stops indexed54,011
Trips loaded495,345
Stop times10,933,796
Patterns (grouped trips)~15,000
Transfer pairs206,822
Index build time10-30 seconds
Index cache size~200 MB
RAM usage (loaded)~500 MB

Pattern Grouping

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):

Benchmark

MetricValue
Min215 ms
Average371 ms
Median370 ms
p95515 ms
Max531 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.

MetricValue
Transfer pairs analyzed71,479
With indoor data3,729 (5.2%)
Outdoor only67,745 (94.8%)
Stations with indoor data355 / 9,047 (3.9%)

Indoor Maneuvers Found

TypeCount
Escalator3,060
Stairs1,441
Elevator1,296
Enter building34
Exit building26

Top 10 Stations by Indoor Score

StationScoreIndoor Ratio
Gare Saint-Lazare1,34453%
La Défense1,08957%
Gare du Nord64652%
Massy - Palaiseau63387%
Gare Montparnasse52727%
Versailles Chantiers47877%
Opéra38434%
République33232%
Juvisy30474%
Gare de l'Est26032%

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.

ModeCostingKey Options
Walkpedestrianstep_penalty: 30, elevator_penalty: 60
City bikebicycle16 km/h, avoid hills and roads
E-bikebicycle21 km/h, hills easy with motor
Road bikebicycle25 km/h, prefer smooth tarmac
CarautoDefault Valhalla settings

Bike Profiles

Three bike profiles are configured for the Ile-de-France context:

ProfileSpeedUse CaseHillsRoads
City (Vélib')16 km/hDense urban areasAvoid (0.3)Avoid (0.2)
E-bike (VAE)21 km/hCommutingEasy (0.8)Accept (0.4)
Road25 km/hFast commutersModerate (0.5)Prefer (0.6)

Coverage Area

The default configuration covers the 8 departments of Ile-de-France:

DepartmentCode
Paris75
Seine-et-Marne77
Yvelines78
Essonne91
Hauts-de-Seine92
Seine-Saint-Denis93
Val-de-Marne94
Val-d'Oise95

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.

How this report was generated

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

MetricValue
Total transfer pairs checked71,479
With indoor routing data3,729 (5.2%)
Outdoor only67,745 (94.8%)
Routing errors5
Stations with indoor data355 / 9,047 (3.9%)

Indoor Maneuver Types

TypeCountDescription
Escalator3,060Escalator transitions between levels
Stairs1,441Staircase transitions
Elevator1,296Elevator/lift transitions
Enter building34Entering a station building
Exit building26Exiting 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).

StationScoreRatioElevatorsStairsEscalators
Gare Saint-Lazare1,34453%85324935
La Défense1,08957%4701,042
Gare du Nord64652%8313325
Massy - Palaiseau63387%2290404
Gare Montparnasse52727%048479
Versailles Chantiers47877%1400338
Opera38434%0228156
Republique33232%028844
Juvisy30474%242062
Gare de l'Est26032%888164
Saint-Quentin en Yvelines24361%145098
Gare de Lyon22625%1065151
Europe21032%013476
Corbeil-Essonnes20658%81980
Bibliotheque Francois Mitterrand19850%293166
Aeroport CDG Terminal 2 (TGV)18068%36066
Fontainebleau - Avon16474%137270
Aulnay-sous-Bois14059%14000
Neuilly - Porte Maillot14018%032108
Magenta12675%413109
Haussmann Saint-Lazare119100%371666
Evry - Courcouronnes8728%21066
Havre - Caumartin8110%162144
Bondy7559%8670
Rosny-sous-Bois6431%0640

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 TagDescription
highway=footwayIndoor walkways/corridors
highway=stepsStaircases between levels
highway=elevatorElevators/lifts
indoor=yesMarks a way as indoor
level=*Floor level (-1, 0, 1, ...)
railway=subway_entranceMetro 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

Regenerate this report

After updating Valhalla tiles with new OSM data, regenerate the CSV files:

python3 bin/check_indoor.py --output data/indoor_report.csv --summary data/indoor_summary.csv