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.

Mileage-band rate lookup data flow An origin and destination pair enters a mileage engine that returns practical miles under a declared mileage type. That distance flows into a band bucketing stage that uses half-open intervals on sorted lower bounds via bisect, selecting exactly one contracted mileage band. The selected band feeds a rate lookup that applies the band's basis, either a per-mile rate or a flat minimum, to produce linehaul. Two failure branches leave the bucketing stage: a distance below the lowest band drops to an intrazone minimum path, and a distance matching no band routes to a quarantine table. Linehaul exits to the validation tier. DISTANCE BUCKET RATE LINEHAUL Origin / destination ZIP3 · carrier_scac ship_date Mileage engine PCMILER / Rand McNally practical miles miles = 512 Band bucketing half-open [lower, upper) bisect on sorted lowers exactly one band Rate lookup band 500–1000 $2.20 / mi or flat minimum Expected linehaul to validation tier miles < lowest band no matching band Intrazone minimum flat floor charge Quarantine band gap · reprocess
The lookup resolves distance once, buckets it into exactly one half-open band, and applies that band's basis to produce linehaul — while a sub-minimum distance drops to the intrazone floor and a band gap routes to quarantine rather than returning a silent zero.

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


Up: Freight Contract Architecture & Rate Mapping