This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

solverforge-maps

Road-network loading, routing, travel-time matrices, and route geometry utilities for vehicle routing applications.

solverforge-maps is SolverForge’s Rust library for map-backed routing workflows. It is designed for vehicle routing and similar optimization problems where you need to turn geographic coordinates into travel times, route geometries, and network diagnostics.

What It Provides

  • OSM-backed road networks loaded from the Overpass API with local caching
  • Validated geographic primitives such as Coord and BoundingBox
  • Routing on road graphs for time- or distance-based objectives
  • Travel-time matrices for feeding VRP and dispatch solvers
  • Route geometries for frontend visualization and polyline transport
  • Connectivity diagnostics for debugging unreachable pairs and bad map regions

Installation

[dependencies]
solverforge-maps = "2"
tokio = { version = "1", features = ["full"] }

Minimal Workflow

use solverforge_maps::{BoundingBox, Coord, NetworkConfig, RoadNetwork, RoutingResult};

#[tokio::main]
async fn main() -> RoutingResult<()> {
    let locations = vec![
        Coord::try_new(39.95, -75.16)?,
        Coord::try_new(39.96, -75.17)?,
        Coord::try_new(39.94, -75.15)?,
    ];

    let bbox = BoundingBox::from_coords(&locations).expand_for_routing(&locations);
    let config = NetworkConfig::default();
    let network = RoadNetwork::load_or_fetch(&bbox, &config, None).await?;

    let matrix = network.compute_matrix(&locations, None).await;
    let route = network.route(locations[0], locations[1])?;

    println!("{} locations", matrix.size());
    println!("Route duration: {} seconds", route.duration_seconds);
    Ok(())
}

route() snaps endpoints to nearby graph nodes. For frontend geometry that should stay anchored to the containing road segments, use snap_to_edge with route_edge_snapped instead.

When To Use It

Use solverforge-maps when your optimization model needs real road travel times instead of crow-flies distance. Typical use cases include:

  • vehicle routing and dispatch
  • field-service scheduling
  • technician assignment with travel
  • territory planning and coverage analysis
  • map-based optimization frontends that need route geometry

Sections

External References

1 - Getting Started

Build your first road-network-backed travel-time matrix with solverforge-maps.

Getting Started with solverforge-maps

This guide covers the standard solverforge-maps workflow:

  1. validate input coordinates
  2. derive a bounding box that covers the relevant area
  3. load a road network from cache or Overpass
  4. compute a travel-time matrix
  5. optionally compute a route for visualization or debugging

Prerequisites

  • Rust stable toolchain
  • An async runtime such as Tokio
  • Internet access the first time a new road network is fetched

Add the Dependency

[dependencies]
solverforge-maps = "2"
tokio = { version = "1", features = ["full"] }

Step 1: Start with Validated Coordinates

Coord::try_new is the right choice for user input, CSV imports, and API payloads because it rejects invalid latitude and longitude values.

use solverforge_maps::Coord;

let depot = Coord::try_new(39.9526, -75.1652)?;
let customer_a = Coord::try_new(39.9610, -75.1700)?;
let customer_b = Coord::try_new(39.9440, -75.1500)?;
let locations = vec![depot, customer_a, customer_b];

Step 2: Build a Routing Bounding Box

Use BoundingBox::from_coords and then expand it for realistic road detours.

use solverforge_maps::BoundingBox;

let bbox = BoundingBox::from_coords(&locations).expand_for_routing(&locations);

That expansion matters because road routes rarely travel in a straight line. A tight bounding box can clip the roads needed for a valid path.

Step 3: Load or Fetch the Road Network

use solverforge_maps::{NetworkConfig, RoadNetwork};

let config = NetworkConfig::default();
let network = RoadNetwork::load_or_fetch(&bbox, &config, None).await?;

load_or_fetch gives you the normal production behavior:

  • reuse the in-memory cache when possible
  • reuse the file cache when the region was fetched before
  • fall back to the Overpass API only when necessary

Step 4: Compute the Travel-Time Matrix

let matrix = network.compute_matrix(&locations, None).await;

println!("Matrix size: {}", matrix.size());

This matrix is the bridge between geospatial data and optimization. A VRP solver can use it as the cost model for sequencing stops, estimating arrival times, and comparing alternative route plans.

Step 5: Route Individual Pairs

let route = network.route(locations[0], locations[1])?;

println!("Duration: {} seconds", route.duration_seconds);
println!("Geometry points: {}", route.geometry.len());

route() snaps both endpoints to the nearest graph nodes before routing. That is a good default for travel-time analysis, but it can produce geometries that start and end at nearby intersections instead of the exact stop positions.

For frontend rendering where the route should stay on the containing road segments, use edge snapping:

let from = network.snap_to_edge(locations[0])?;
let to = network.snap_to_edge(locations[1])?;
let route = network.route_edge_snapped(&from, &to)?;

Full Example

use solverforge_maps::{BoundingBox, Coord, NetworkConfig, RoadNetwork, RoutingResult};

#[tokio::main]
async fn main() -> RoutingResult<()> {
    let locations = vec![
        Coord::try_new(39.9526, -75.1652)?,
        Coord::try_new(39.9610, -75.1700)?,
        Coord::try_new(39.9440, -75.1500)?,
    ];

    let bbox = BoundingBox::from_coords(&locations).expand_for_routing(&locations);
    let config = NetworkConfig::default();
    let network = RoadNetwork::load_or_fetch(&bbox, &config, None).await?;

    let matrix = network.compute_matrix(&locations, None).await;
    let route = network.route(locations[0], locations[1])?;

    println!("Matrix size: {}", matrix.size());
    println!("Route duration: {} seconds", route.duration_seconds);

    Ok(())
}

Common Next Steps

After the first matrix is working, most applications move on to one or more of these tasks:

  • inspect route geometry in a frontend map
  • check cache hit rates for repeated workloads
  • compute full matrices for solver input
  • tune NetworkConfig for production environments

Read Core Types, Routing & Matrices, and Caching & Operations for those topics.

2 - Routing & Matrices

Compute routes, choose objectives, and build matrix data for optimization.

Routing & Matrices

This page covers the methods most applications call after the network is loaded.

Route a Single Pair

let route = network.route(locations[0], locations[1])?;

println!("Duration: {} seconds", route.duration_seconds);
println!("Geometry points: {}", route.geometry.len());

This is the simplest path from validated coordinates to a route result.

route() snaps both endpoints to the nearest graph nodes first. That is usually fine for travel-time estimation, but it can shift the visible start or end of the geometry to a nearby intersection.

Preserve Road-Segment Endpoints

When you need the rendered geometry to begin and end on the containing road segments, use edge snapping instead of node snapping.

let from = network.snap_to_edge(locations[0])?;
let to = network.snap_to_edge(locations[1])?;
let route = network.route_edge_snapped(&from, &to)?;

This is the better choice for map previews, route-detail UIs, and stop-level visualizations.

Choose a Routing Objective

The crate exposes an Objective type for routing decisions. In practice, most optimization workflows choose between:

  • time-oriented routing for dispatch and service scheduling
  • distance-oriented routing for mileage comparisons and diagnostics

When your solver minimizes time windows, lateness, or shift utilization, time-based routing is usually the right default.

use solverforge_maps::Objective;

let route = network.route_with(locations[0], locations[1], Objective::Distance)?;

Compute a Travel-Time Matrix

let matrix = network.compute_matrix(&locations, None).await;
println!("Locations: {}", matrix.size());

The matrix is the most common output for optimization systems because it lets the solver compare every stop against every other stop using realistic road travel instead of straight-line distance.

Geometry for Frontend Visualization

RouteResult includes geometry that can be rendered directly on a web map. For compact transmission, solverforge-maps also exposes Google polyline helpers.

use solverforge_maps::{decode_polyline, encode_polyline};

let encoded = encode_polyline(&route.geometry);
let decoded = decode_polyline(&encoded);

This is a good fit when your architecture computes routes in Rust but renders them in a browser or mobile client.

Connectivity Diagnostics

A routing failure does not always mean the input is invalid. It may mean the graph is fragmented.

let components = network.strongly_connected_components();
let largest_fraction = network.largest_component_fraction();

These metrics are useful when evaluating a new operating area or diagnosing a matrix with many unreachable pairs.

Practical Advice

  • Use matrices when your solver will evaluate many alternative stop orders.
  • Use single-route calls when you need a detailed geometry or debugging trace.
  • Expand your bounding box if you see unexpected routing failures at the edges of a region.
  • Inspect graph connectivity before assuming the optimization layer is at fault.

3 - Caching & Operations

Cache inspection, progress reporting, and production-oriented diagnostics.

Caching & Operations

solverforge-maps is built for repeated routing over shared map regions. The operational APIs help you keep those workloads fast and observable.

Cache Layers

load_or_fetch checks three places in order:

  1. check the in-memory cache
  2. check the file cache
  3. fetch from the Overpass API when the region is not cached

Repeat runs for the same service area can become much faster after the first network build.

Inspect Cache Stats

let stats = RoadNetwork::cache_stats().await;
println!("Networks cached: {}", stats.networks_cached);
println!("Nodes: {}, edges: {}", stats.total_nodes, stats.total_edges);
println!("In-memory hits: {}, misses: {}", stats.hits, stats.misses);

cache_stats() is most useful for understanding the current in-memory cache footprint.

The hits and misses counters are coarse process-level counters, not a precise breakdown of memory hits versus file-cache loads versus Overpass fetches.

Progress Reporting

The crate exposes RoutingProgress so long-running operations can report status while a network is loading or a matrix is being built.

That is useful for:

  • CLI tools that need progress output
  • web backends that stream job status
  • admin screens that expose long-running matrix builds

Connectivity and Failure Analysis

When a route or matrix result is surprising, check the network before changing your solver model.

Good first questions are:

  • Is the bounding box large enough for realistic detours?
  • Is the graph strongly connected in the area you care about?
  • Are some locations landing in disconnected subgraphs?
  • Is the current process reusing cached regions for repeated work?
  • Use Coord::try_new and BoundingBox::try_new on external input.
  • Keep a stable cache location for repeated workloads.
  • Warm commonly used service areas ahead of large optimization jobs.
  • Treat cache hit/miss counters as coarse indicators, not exact layer-specific metrics.
  • Use connectivity diagnostics when a region produces many unreachable pairs.