logistics.route-distance
Total great-circle distance of an ordered list of stops, leg by leg, optionally back to the start, to the millimetre.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 12 tests, run in TypeScript, Python and Rust.
What it does
The length of a route that visits the given stops in the given order: the great-circle distance of each leg from `geo.distance`, and their total. With `returnToStart` a closing leg from the last stop back to the first is added, for a van that comes back to the depot.
It does not choose the order (that is route optimisation, a much bigger problem) and it measures as the crow flies. Road distance is typically 20-40% longer; callers that quote it multiply by their own circuity factor, and callers that need the real figure need a routing engine.
For example
route_distance(stops ×2, false)→ total metres 343,556.535, leg metres 343,556.535 London to Paris is one legroute_distance(stops ×2, true)→ total metres 687,113.07, leg metres 343,556.535, 343,556.535 round trip adds the leg homeroute_distance(stops ×3, false)→ total metres 222,390.16, leg metres 111,195.08, 111,195.08 along the equator then north, a degree each
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn route_distance(stops: &[GeoPoint], return_to_start: bool) -> RouteDistance
| stops | GeoPoint[] | in the order they are visited; at least one |
| return_to_start | bool | add a closing leg from the last stop back to the first, for a round trip |
| returns | RouteDistance |
The type it declares, generated into your project
/// The whole route and each leg, in metres to the millimetre.
#[derive(Debug, Clone, PartialEq)]
pub struct RouteDistance {
/// the sum of the legs, summed exactly in millimetres
pub total_metres: f64,
/// one per leg: stops[0] to stops[1], stops[1] to stops[2], ...
pub leg_metres: Vec<f64>,
}
Your code names it in one line, in the file that uses it
fune!(logistics.route-distance@^1); // then call route_distance(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::geo_distance::distance; ← from geo.distance ^1.0.0 · built alongside by fune
use super::geo_point_in_polygon::{geo_point_from_value, GeoPoint}; ← from geo.point-in-polygon ^1.0.0 · built alongside by fune
/// Great-circle length of a route through the stops in order, leg by leg,
/// optionally closing back to the first stop.
///
/// # Panics
/// Panics on an empty list or a coordinate out of range.
pub fn route_distance(stops: &[GeoPoint], return_to_start: bool) -> RouteDistance {
if stops.is_empty() {
panic!("stops must not be empty");
}
let mut leg_metres: Vec<f64> = Vec::new();
// Each leg is a whole number of millimetres; adding them as integers and
// dividing once avoids floating-point drift (and is the same double everywhere).
let mut total_millimetres: i64 = 0;
let mut add_leg = |a: &GeoPoint, b: &GeoPoint| {
let metres = distance(a.lat, a.lng, b.lat, b.lng);
leg_metres.push(metres);
total_millimetres += (metres * 1000.0 + 0.5).floor() as i64;
};
for i in 1..stops.len() {
add_leg(&stops[i - 1], &stops[i]);
}
if return_to_start && stops.len() > 1 {
add_leg(&stops[stops.len() - 1], &stops[0]);
}
RouteDistance {
total_metres: total_millimetres as f64 / 1000.0,
leg_metres,
}
}
pub fn route_distance_to_value(r: &RouteDistance) -> Value {
Value::obj(vec![
("totalMetres", Value::Float(r.total_metres)),
("legMetres", Value::Arr(r.leg_metres.iter().map(|m| Value::Float(*m)).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let stops: Vec<GeoPoint> = args[0].as_arr().iter().map(geo_point_from_value).collect();
route_distance_to_value(&route_distance(&stops, args[1].as_bool()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add logistics.route-distance
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./logistics.route-distance-1.0.0-rust.fune, or fetch it from a terminal with fune pull logistics.route-distance@1.0.0:rust.
The whole function, every language, is one file too: logistics.route-distance-1.0.0.fune, 10,910 bytes, sha256 466edb4c820220ce3afbcc8bdacd67502202c89f2f192576ce8b885c7f53a9a8. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before logistics.route-distance
after — your function gets the result and the arguments, and returns the final result.
// fune: after logistics.route-distance
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace geo.distance in logistics.route-distance
// fune: replace geo.point-in-polygon in logistics.route-distance
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show logistics.route-distance --steps.
// fune: step logistics.route-distance after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| London to Paris is one leg | stops ×2, false | → | total metres 343,556.535, leg metres 343,556.535 |
| round trip adds the leg home | stops ×2, true | → | total metres 687,113.07, leg metres 343,556.535, 343,556.535 |
| along the equator then north, a degree each | stops ×3, false | → | total metres 222,390.16, leg metres 111,195.08, 111,195.08 |
| three legs summed in floating point would be 8659345.290000001 | stops ×4, false | → | total metres 8,659,345.29, leg metres 2,886,448.43, 2,886,448.43, 2,886,448.43 |
| five legs summed in floating point would be 1717782.6749999998 | stops ×6, false | → | total metres 1,717,782.675, leg metres 343,556.535, 343,556.535, 343,556.535, 343,556.535, 343,556.535 |
| across the antimeridian is one degree, not 359 | stops ×2, false | → | total metres 111,195.08, leg metres 111,195.08 |
| a repeated stop is a leg of zero | stops ×3, false | → | total metres 343,556.535, leg metres 0, 343,556.535 |
| one stop has no legs | stops ×1, false | → | total metres 0, leg metres |
| one stop has nowhere to return from | stops ×1, true | → | total metres 0, leg metres |
| no stops is an error, not zero | , false | → | error: stops must not be empty |
Show the other 2 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a latitude out of range is an error | stops ×3, false | → | error: latitude must be between -90 and 90 degrees |
| a longitude out of range is an error | stops ×2, false | → | error: longitude must be between -180 and 180 degrees |
More from the author
## Exact totals
Each leg is `geo.distance`'s answer, a whole number of millimetres expressed in metres. Adding those as floating-point numbers drifts: three legs of 2,886,448.43 m add up to 8,659,345.290000001 m. So each leg is turned back into integer millimetres, the millimetres are added as integers, and the total is divided by 1000 once, which gives the same double in every language (8,659,345.29). The manifest says `floats exact` and the vectors hold the three implementations to it.
## Edge cases
- One stop is a route of no legs: total 0, `legMetres` empty, with or without `returnToStart` (there is nowhere to return from). - Two identical stops in a row are a leg of 0. - An empty list is an error, not 0: it usually means the stops were never loaded. - Coordinates are checked by `geo.distance`: out of range is an error, not wrapped, because it usually means latitude and longitude were swapped.
## Types
A stop is `GeoPoint` from `geo.point-in-polygon`, the registry's one latitude/longitude record, so a polygon's vertices and a route's stops are the same type. That capability is required for its type only.
Files
| Path | Bytes |
|---|---|
| README.md | 1,715 |
| impl/python.py | 1,176 |
| impl/rust.rs | 1,698 |
| impl/typescript.ts | 1,127 |
| vectors.json | 2,679 |