Metadata-Version: 2.3
Name: distance-calculation-cli
Version: 0.5.0
Summary: CLI for computing drive distance and drive time between US addresses
Author: Ryan Wiley
Author-email: Ryan Wiley <wileyrya@gmail.com>
Requires-Dist: typer>=0.12
Requires-Dist: httpx>=0.27
Requires-Dist: rich>=13.7
Requires-Dist: tenacity>=8.2
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# distance-calculation-cli

Compute drive distance and drive time between US addresses, one pair at a time
or in bulk from a CSV of origin/destination pairs.

- **Geocoding:** US Census Geocoder (free, keyless, TIGER-based), with
  OpenStreetMap Nominatim as a fallback for addresses Census can't match.
- **Routing:** [OSRM](http://project-osrm.org/) — the public demo server by
  default (politely throttled), or a self-hosted local containerized OSRM instance
  for large batch jobs.

## Quickstart

```bash
uvx distance-calculation-cli "1 Campus Martius, Detroit, MI" "300 Ottawa Ave NW, Grand Rapids, MI"
```

```
Routing via: https://router.project-osrm.org (public demo server, throttled)
Origin matched: 1 CAMPUS MARTIUS, DETROIT, MI, 48226
Destination matched: 300 OTTAWA AVE NW, GRAND RAPIDS, MI, 49503
Distance: 157.5 miles
Drive time: 2h 56m
```

Batch mode:

```bash
uvx distance-calculation-cli --csv addresses.csv -o results.csv
```

## Installation

```bash
uv tool install distance-calculation-cli
# or, one-off:
uvx distance-calculation-cli ...
```

## CSV format

Input CSV must have (at minimum) an origin and destination address column,
named `origin_address` / `destination_address` by default (override with
`--origin-col` / `--destination-col`):

| origin_address | destination_address |
|---|---|
| 1 Campus Martius, Detroit, MI | 300 Ottawa Ave NW, Grand Rapids, MI |
| 2 Woodward Ave, Detroit, MI | 124 W Michigan Ave, Lansing, MI |

Any other input columns are preserved as-is. The output CSV adds:

| column | meaning |
|---|---|
| `matched_origin_address` | Address as matched by the geocoder |
| `origin_lat`, `origin_lon` | Geocoded origin coordinates |
| `matched_destination_address` | Address as matched by the geocoder |
| `destination_lat`, `destination_lon` | Geocoded destination coordinates |
| `distance_miles` | Drive distance |
| `drive_time_minutes` | Drive time |
| `status` | See status reference below |
| `error_detail` | Human-readable failure reason, if any |
| `routing_server` | OSRM server used for this row |

Rows are written incrementally as they complete (crash-safe, bounded memory),
with a progress bar when running in a terminal. Failed rows are flagged, never
dropped.

### Status reference

| status | meaning |
|---|---|
| `ok` | Geocoded and routed successfully |
| `geocode_failed_origin` | Could not geocode the origin address |
| `geocode_failed_destination` | Could not geocode the destination address |
| `geocode_failed_both` | Could not geocode either address |
| `route_failed` | Both addresses geocoded, but OSRM found no route |
| `invalid_input_row` | Row was missing an origin or destination value |

## Options

| option | env var | default | meaning |
|---|---|---|---|
| `--osrm-url` | `DISTANCE_CLI_OSRM_URL` | (auto-resolved) | OSRM server to route against |
| `--osrm-rps` | | (server-dependent) | Override the OSRM requests/second cap |
| `--geocoder-url` | `DISTANCE_CLI_GEOCODER_URL` | (auto-resolved) | Self-hosted Nominatim to use as the primary geocoder |
| `--no-throttle` | | off | Disable OSRM rate limiting entirely |
| `--concurrency` | | 20 | Max concurrent geocode/route requests |
| `--origin-col` / `--destination-col` | | `origin_address` / `destination_address` | CSV column names |
| `--units` | | `miles` | `miles` or `km` |
| `--no-fallback` | | off | Disable the Nominatim fallback geocoder |
| `--nominatim-user-agent` | `DISTANCE_CLI_NOMINATIM_UA` | | Required to enable Nominatim fallback |
| `--retries` | | 3 | Retry attempts for transient errors |
| `--timeout` | | 15s | Per-request timeout |
| `--cache-dir` | | | Enable a persistent SQLite geocode cache (90-day TTL) |
| `--dry-run` | | off | Parse + dedupe the input only; no network calls |
| `--resume` | | off | Skip rows already `ok` in an existing output file |
| `--public` | | off | Force the public OSRM server even if local OSRM is running |
| `--no-probe` | | off | Skip probing for local OSRM/Nominatim instances |
| `--yes` | | off | Skip confirmation prompts (for scripts) |

### Routing server resolution order

1. `--osrm-url` flag
2. `DISTANCE_CLI_OSRM_URL` environment variable
3. Probe `http://localhost:5000` for a running local OSRM instance
4. Fall back to the public `router.project-osrm.org` demo server

A loopback server is used unthrottled; the public server is capped at 1
request/second; any other custom URL is throttled by default (use
`--no-throttle` if you know it can take more). The CLI prints which server
it's using at startup, e.g. `Routing via: http://localhost:5000 (local OSRM,
unthrottled)`.

## Agent skill (Claude Code)

To teach a coding agent how to drive this CLI — command reference, server
resolution behavior, resource requirements, and common failure modes — install
the bundled skill:

```bash
distance-calculation-cli skill install            # ~/.claude/skills (all sessions)
distance-calculation-cli skill install --project  # ./.claude/skills (this repo only)
distance-calculation-cli skill show               # preview the content
distance-calculation-cli skill uninstall
```

New Claude Code sessions pick the skill up automatically; use `--force` to
overwrite after upgrading the CLI.

## Rate-limit etiquette

The Census geocoder and the public OSRM demo server are free, shared, keyless
services — please be a good citizen. For a few one-off lookups the defaults
are fine. For tens of thousands of pairs, run a local OSRM instance (see
below) and consider a `--cache-dir` for geocoding.

Nominatim's usage policy requires a descriptive `User-Agent` identifying your
application; without one (`--nominatim-user-agent` or
`DISTANCE_CLI_NOMINATIM_UA`), the Nominatim fallback is disabled and a warning
is printed — Census-only results still work, just with a lower match rate.

## Local OSRM (for large batches)

For workloads of tens of thousands of pairs, run OSRM locally instead of
hammering the public server:

```bash
distance-calculation-cli osrm setup us/michigan   # download + process (one-time)
distance-calculation-cli osrm start us/michigan   # runs osrm-routed in a container
distance-calculation-cli "..." "..."              # auto-detects localhost:5000
distance-calculation-cli osrm status
distance-calculation-cli osrm stop
```

`REGION` is a [Geofabrik](https://download.geofabrik.de/north-america.html)
path fragment, e.g. `us/michigan`. Michigan is a good first example — small,
fast to process. `us-latest` (the whole US) needs 64GB+ of RAM to process and
requires `--yes` to proceed. Requires a container engine — Podman is preferred
if installed, otherwise Docker (override with `--engine` or
`DISTANCE_CLI_CONTAINER_ENGINE`). Data is stored under
`~/.local/share/distance-calculation-cli/osrm/`.

Once a local OSRM instance is running, it's auto-detected via a health probe
of `localhost:5000` — no extra flags needed.

## Local geocoding (fully offline batches)

Routing locally removes one bottleneck, but geocoding still runs at 1–5
requests/second against shared public services. To go fully local, run a
self-hosted [Nominatim](https://nominatim.org/) for the same region:

```bash
distance-calculation-cli geocoder start us/michigan  # first start imports the data
distance-calculation-cli "..." "..."                 # auto-detects localhost:8080
distance-calculation-cli geocoder status
distance-calculation-cli geocoder stop               # imported data is kept
distance-calculation-cli geocoder purge us/michigan  # delete the imported data
```

The region's `.osm.pbf` download is shared with `osrm setup`. On the first
start, Nominatim imports the extract into PostgreSQL inside the container —
for a state-sized region this takes 15–60+ minutes, needs 8GB+ of RAM, and
uses ~10–20GB of disk (kept in a named container volume so later starts are
fast). Progress and the current import stage are shown while it runs.

When a local Nominatim is running (or `--geocoder-url` /
`DISTANCE_CLI_GEOCODER_URL` points at one), it becomes the *primary*
geocoder, unthrottled, with the Census geocoder as fallback — no User-Agent
required. Resolution order mirrors routing: flag → env var → probe
`localhost:8080` → default Census-first behavior.

Note that OSM address coverage is thinner than the Census TIGER data in some
US areas; keeping the Census fallback (the default) gives the best match
rate.

## Geocode-only mode

To resolve addresses to coordinates without routing:

```bash
distance-calculation-cli geocode "800 W Main St, Midland, MI"
distance-calculation-cli geocode "800 W Main St, Midland, MI" --json
distance-calculation-cli geocode --csv addresses.csv -o geocoded.csv
```

Batch mode reads an `address` column (override with `--address-col`),
dedupes, and writes the input columns plus `status`, `matched_address`,
`lat`, `lon`, and `source`. It uses the same geocoder resolution as the
distance command (local Nominatim auto-detected, Census otherwise, same
`--cache-dir` cache) and exits nonzero if any address failed.
