Mileage-Band Rate Lookups
Distance-based pricing is where a truckload audit quietly loses money. A carrier tenders a lane, bills a linehaul figure, and the audit engine has to answer two questions in sequence: how far is this move under the contract’s mileage rules, and which contracted rate applies to that distance? Both are easy to get subtly wrong. Practical routing miles and shortest-distance miles diverge by five to fifteen percent on the same origin-destination pair, so a lookup keyed on the wrong mileage type lands the shipment in the wrong band and validates a real overcharge as compliant. And because most negotiated truckload and volume tariffs price distance in stepped bands rather than a single per-mile figure, the exact boundary semantics — is a 500-mile move in the 250–500 band or the 500–1000 band? — decide the rate every time a shipment falls on an edge.
This page covers the resolution layer that sits between a normalized shipment and the linehaul figure the validation tier checks. It takes an origin/destination pair, computes practical miles through a mileage engine such as PCMILER or Rand McNally, buckets that distance into the carrier’s contracted mileage bands using half-open interval semantics, and returns the banded rate that produces expected linehaul. It is the distance-specific companion to lane-flat pricing in FTL Base Rate Extraction, and it reads its band tables from the same versioned store described in Freight Contract Architecture & Rate Mapping. Get the mileage type and the band boundaries right and the linehaul check downstream is deterministic; get either wrong and every mid-band shipment is mispriced by a margin no tolerance band will catch.
Prerequisites
This stage assumes distance is a resolved input, not something it computes from raw geocodes itself. A mileage engine export — practical miles per origin/destination pair keyed by ZIP3 or ZIP5 — must already be loaded, and the carrier’s band table must be digitized into a sorted, non-overlapping structure before any lookup runs.
| Prerequisite | Source | Contract |
|---|---|---|
| Mileage engine export | PCMILER / Rand McNally MileMaker | origin_zip, dest_zip, practical_miles, mileage_version |
| Band table | Negotiated TL/volume tariff | ordered (lower_incl, upper_excl, basis, amount) rows per carrier_scac |
| Contract version | Versioned contract store | contract_version_id pinning the band table to the ship date |
| Mileage type policy | Contract terms | PRACTICAL, SHORTEST, or HG (household-goods) miles |
| Python dependency | Minimum version | Role in this stage |
|---|---|---|
python |
3.10+ | match, modern typing, bisect key support |
bisect (stdlib) |
— | O(log n) band lookup on sorted lower bounds |
decimal (stdlib) |
— | Exact linehaul arithmetic, no float drift |
polars / pandas |
optional | Vectorized band assignment over large batches |
The single most important config key is the mileage type. A contract that prices on practical miles must be resolved with a practical-miles export; feeding it shortest miles understates every distance and drops long moves into a cheaper band. That mismatch is the root failure that Building Mileage-Band Rate Lookups from PCMILER Exports exists to prevent.
Architecture
The lookup is a short, strictly ordered pipeline. An origin/destination pair resolves to a practical-miles figure through the mileage engine, that distance is bucketed into exactly one band, and the band’s rate basis produces linehaul. The two failure exits — a distance below the lowest band and a distance with no matching band — are explicit branches, not silent nulls.
The band table itself is the reference structure the whole cluster keys on. A worked example for carrier ABCD shows the half-open convention that every stage below assumes: each band owns its lower bound and excludes its upper bound, so the bounds chain without gaps or overlaps.
| Band | Lower (mi, incl) | Upper (mi, excl) | Basis | Amount |
|---|---|---|---|---|
| B0 | 0 | 50 | flat minimum | $195.00 |
| B1 | 50 | 150 | per mile | $3.05 |
| B2 | 150 | 500 | per mile | $2.62 |
| B3 | 500 | 1000 | per mile | $2.20 |
| B4 | 1000 | ∞ | per mile | $1.94 |
A 500-mile move belongs to B3, not B2, because B2’s upper bound of 500 is exclusive. Encoding that convention once, in the band structure, is what keeps the boundary decision out of every call site.
Step-by-step implementation
Step 1 — Load and validate the band table
Load the contracted bands into a structure that is sorted by lower bound and proven contiguous at construction time. Validating contiguity here — rather than trusting the digitized source — is what catches the off-by-one gaps that otherwise surface as production quarantines weeks later.
# mileage/band_table.py
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from typing import List, Tuple
class Basis(str, Enum):
PER_MILE = "PER_MILE"
FLAT = "FLAT"
@dataclass(frozen=True)
class Band:
lower_incl: int # miles, inclusive
upper_excl: float # miles, exclusive (float so the top band can use inf)
basis: Basis
amount: Decimal
class MileageBandTable:
def __init__(self, bands: List[Band]) -> None:
if not bands:
raise ValueError("band table is empty")
ordered = sorted(bands, key=lambda b: b.lower_incl)
# Contiguity check: each band's upper must equal the next band's lower,
# so there is no gap (unpriced miles) and no overlap (ambiguous rate).
for prev, nxt in zip(ordered, ordered[1:]):
if prev.upper_excl != nxt.lower_incl:
raise ValueError(
f"band gap/overlap: {prev.upper_excl} != {nxt.lower_incl}"
)
self._bands: Tuple[Band, ...] = tuple(ordered)
self._lowers: Tuple[int, ...] = tuple(b.lower_incl for b in ordered)
Common mistake: loading bands in spreadsheet order and trusting it. A band table pasted from a tariff PDF is frequently row-shuffled; sorting at construction and asserting contiguity turns a silent misprice into a load-time exception.
Step 2 — Resolve practical miles
Distance comes from the mileage engine keyed to the contract’s declared mileage type. Resolving with the wrong type is the highest-impact error in this stage, so the resolver refuses to guess: if the export’s mileage_version does not match the type the contract expects, it raises rather than returning a plausible-but-wrong number.
# mileage/distance.py
from dataclasses import dataclass
@dataclass(frozen=True)
class MileageResult:
miles: int
mileage_type: str # PRACTICAL | SHORTEST | HG
engine_version: str
def resolve_miles(export: dict, origin_zip: str, dest_zip: str,
expected_type: str) -> MileageResult:
"""Look up practical miles, refusing a mileage-type mismatch."""
key = (origin_zip[:3], dest_zip[:3]) # ZIP3 lane key
row = export.get(key)
if row is None:
raise KeyError(f"no mileage row for lane {key}")
if row["mileage_type"] != expected_type:
# A practical-priced contract must not be resolved on shortest miles.
raise ValueError(
f"mileage type mismatch: export={row['mileage_type']} "
f"contract expects {expected_type}"
)
return MileageResult(
miles=int(row["miles"]),
mileage_type=row["mileage_type"],
engine_version=row["engine_version"],
)
Common mistake: defaulting a missing lane to zero miles. Zero miles buckets into the lowest band and bills the intrazone minimum on a move that may span three states — always raise on a missing lane so it quarantines instead.
Step 3 — Bucket the distance into one band
With a sorted lower-bound array, bisect_right finds the band in O(log n) and the half-open convention falls out naturally: the insertion point minus one is the band whose lower bound is the greatest value not exceeding the distance.
# mileage/bucket.py
import bisect
from typing import Optional
def band_for(table: "MileageBandTable", miles: int) -> Optional["Band"]:
"""Return the single band owning `miles`, or None if below the floor."""
lowers = table._lowers
if miles < lowers[0]:
return None # below lowest band -> intrazone path
# bisect_right gives the count of lowers <= miles; minus one is the index.
idx = bisect.bisect_right(lowers, miles) - 1
band = table._bands[idx]
# Half-open guard: miles must be strictly below the band's upper bound.
if miles >= band.upper_excl:
return None # only reachable on a real band gap
return band
Common mistake: using bisect_left or testing lower <= miles <= upper with an inclusive upper. Both put a boundary shipment — exactly 500 miles — into the lower, cheaper band. bisect_right with an exclusive upper is the only combination that keeps 500 in B3.
Step 4 — Resolve the banded rate into linehaul
The band’s basis decides the arithmetic. A per-mile band multiplies distance by the rate; a flat band ignores distance entirely. Keeping this in Decimal avoids the sub-cent drift that produces phantom variance downstream.
# mileage/linehaul.py
from decimal import Decimal, ROUND_HALF_UP
def compute_linehaul(band: "Band", miles: int) -> Decimal:
"""Apply the band basis to produce expected linehaul."""
if band.basis is Basis.FLAT:
charge = band.amount # distance-independent floor
else:
charge = band.amount * Decimal(miles) # per-mile band
return charge.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def expected_linehaul(table: "MileageBandTable", miles: int) -> Decimal:
band = band_for(table, miles)
if band is None:
if miles < table._lowers[0]:
return table._bands[0].amount # intrazone minimum floor
raise ValueError(f"no band covers {miles} miles") # quarantine
return compute_linehaul(band, miles)
Validation and testing
The tests that matter here are boundary tests. Every band edge is a place where an off-by-one silently reprices, so assert the exact-edge and one-below-edge cases explicitly rather than sampling mid-band values that never exercise the semantics.
import pytest
from decimal import Decimal
BANDS = [
Band(0, 50, Basis.FLAT, Decimal("195.00")),
Band(50, 150, Basis.PER_MILE, Decimal("3.05")),
Band(150, 500, Basis.PER_MILE, Decimal("2.62")),
Band(500, 1000, Basis.PER_MILE, Decimal("2.20")),
Band(1000, float("inf"), Basis.PER_MILE, Decimal("1.94")),
]
def test_exact_boundary_goes_to_upper_band():
table = MileageBandTable(BANDS)
# 500 is excluded from the 150-500 band, included in 500-1000.
assert band_for(table, 500).amount == Decimal("2.20")
assert band_for(table, 499).amount == Decimal("2.62")
def test_below_floor_returns_intrazone_minimum():
table = MileageBandTable(BANDS)
assert band_for(table, 12) is None
assert expected_linehaul(table, 12) == Decimal("195.00")
def test_contiguity_violation_rejected_at_load():
gapped = [Band(0, 50, Basis.FLAT, Decimal("195")),
Band(60, 150, Basis.PER_MILE, Decimal("3.05"))]
with pytest.raises(ValueError):
MileageBandTable(gapped) # 50 != 60 -> gap
def test_open_top_band_covers_long_haul():
table = MileageBandTable(BANDS)
assert compute_linehaul(band_for(table, 2400), 2400) == Decimal("4656.00")
The fixture set worth maintaining covers each band edge, the sub-floor intrazone case, a distance in the open-ended top band, and a deliberately gapped table that must fail to load. A regression that flips a boundary surfaces as a failing edge assertion, not as a reconciliation discrepancy a quarter later.
Performance and tuning
Per-invoice, a bisect lookup is nanoseconds — the cost is entirely in how batches are shaped. For month-end runs of hundreds of thousands of shipments, vectorize the band assignment rather than calling band_for in a Python loop.
| Knob | Typical range | Effect | Watch for |
|---|---|---|---|
| Band lookup | bisect per row vs vectorized cut |
Per-row is fine to ~10k; vectorize above | Python-loop overhead dominates at scale |
| Mileage export cache | in-memory dict keyed by ZIP3 | Removes repeated engine reads | Stale cache after a mileage-version bump |
| Batch size | 250–1,000 shipments | Memory vs scheduling overhead | Wide join frames at large sizes |
| Boundary representation | half-open once, in the table | Zero per-call boundary logic | Re-deriving edges at call sites |
A vectorized assignment maps every band’s lower bound onto a sorted cut, so a full batch buckets in one pass. In Polars the idiom is a cut over the sorted lower bounds, evaluated lazily so the engine assigns bands column-wide without materializing intermediate frames; in pandas, pd.cut with right=False reproduces the exact half-open convention. The rule is that the boundary semantics live in the table definition, not re-implemented per engine.
Failure modes
| Symptom | Root cause | Resolution |
|---|---|---|
| Boundary shipments priced one band low | Inclusive upper bound / bisect_left |
Half-open bands + bisect_right; test exact edges |
| Long moves bill the intrazone minimum | Missing lane defaulted to zero miles | Raise on missing lane; never default distance to 0 |
| Sporadic zero linehaul on valid lanes | Band gap between digitized rows | Assert contiguity at load; quarantine the gap |
| Every distance off by ~10% | Practical vs shortest miles mismatch | Enforce mileage_type against the contract |
| Rate drifts after a re-run | Band table not pinned to a contract version | Resolve the table via contract_version_id |
Zero-mile intrazone moves deserve special attention: a shipment whose origin and destination share a ZIP3 legitimately resolves to zero or near-zero miles, and it should bill the flat minimum rather than quarantine. The distinction is that a resolved zero is valid and a missing lane is not — which is why Step 2 raises on absence instead of returning zero.
Integration points
The output of this stage is a single expected_linehaul figure plus the band and mileage type that produced it — a stable contract the validation tier consumes without re-deriving distance. That figure feeds directly into Rule-Based Rate Validation & Accessorial Auditing, which compares billed linehaul against it within tolerance. Because the band table is versioned, the linehaul is only meaningful alongside the contract_version_id resolved through Contract Versioning & Effective Dating — the same mileage-band table can price differently before and after a general rate increase, and pinning the version is what keeps a historical re-run deterministic.
Lane geometry is confirmed in parallel: the origin/destination pair this stage resolves distance for is the same pair validated against contracted lanes by Lane Matching Algorithms, so a lane that fails geometry matching never reaches band bucketing with a bad key.
| Output field | Type | Consumed by |
|---|---|---|
expected_linehaul |
Decimal |
Rate validation |
band_id |
str | Audit trail, dispute payload |
mileage_type |
str | Reconciliation, dispute defense |
contract_version_id |
str | Point-in-time consistency |
In this section
- Building Mileage-Band Rate Lookups from PCMILER Exports — turns a raw mileage-engine export into a band-keyed lookup structure without the practical-vs-shortest mismatch, ambiguous boundaries, and unsorted rows that misfile rates on load.
- Interpolating Mileage-Break Rates in Python — handles contracts that quote rates at discrete mileage break points and expect linear interpolation between them, with a Decimal-safe bracket search and an explicit extrapolation policy.
Related
- Freight Contract Architecture & Rate Mapping — the parent architecture whose versioned store holds the band tables this stage resolves.
- FTL Base Rate Extraction — lane-flat and mileage-band FTL pricing extraction that feeds this stage its band tables.
- Contract Versioning & Effective Dating — pins each band table to the tariff in force on the ship date.
- Lane Matching Algorithms — confirms the origin/destination pair before distance is resolved against it.
- Rule-Based Rate Validation & Accessorial Auditing — the tier that checks billed linehaul against the expected figure produced here.