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:
- validate input coordinates
- derive a bounding box that covers the relevant area
- load a road network from cache or Overpass
- compute a travel-time matrix
- 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:
- check the in-memory cache
- check the file cache
- 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?
Recommended Production Practices
- 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.