{"id":51612203,"url":"https://github.com/elkingarcia11/route-optimizer","last_synced_at":"2026-07-12T09:30:46.816Z","repository":{"id":369123925,"uuid":"1287835688","full_name":"elkingarcia11/route-optimizer","owner":"elkingarcia11","description":"Reorder a list of stops to minimize total travel distance on real roads, with a fixed start and end point.","archived":false,"fork":false,"pushed_at":"2026-07-03T17:48:57.000Z","size":35,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-03T19:07:13.162Z","etag":null,"topics":["distance-matrix","guided-local-search","openrouteservice","or-tools","road-network","route-optimization","tsp","vehicle-routing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elkingarcia11.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-03T03:40:17.000Z","updated_at":"2026-07-03T17:49:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/elkingarcia11/route-optimizer","commit_stats":null,"previous_names":["elkingarcia11/route-optimizer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/elkingarcia11/route-optimizer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elkingarcia11%2Froute-optimizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elkingarcia11%2Froute-optimizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elkingarcia11%2Froute-optimizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elkingarcia11%2Froute-optimizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elkingarcia11","download_url":"https://codeload.github.com/elkingarcia11/route-optimizer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elkingarcia11%2Froute-optimizer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35388394,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-12T02:00:06.386Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distance-matrix","guided-local-search","openrouteservice","or-tools","road-network","route-optimization","tsp","vehicle-routing"],"created_at":"2026-07-12T09:30:45.946Z","updated_at":"2026-07-12T09:30:46.808Z","avatar_url":"https://github.com/elkingarcia11.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Route Optimizer\n\nOptimize a single route from a fixed start through all stops to a fixed end, using real road networks from OpenRouteService.\n\nGiven **start**, **end**, and at least **two stops** (HTTP API) or coordinates (CLI), the service finds the visit order that **minimizes total drive time** using Google OR-Tools.\n\n## How it works\n\n1. **OpenRouteService** builds road **distance** (meters) and **duration** (seconds) matrices between all locations.\n2. **Google OR-Tools** minimizes total **drive time** (Guided Local Search, 5s default) and returns the optimized visit order.\n3. The response reports both **total drive time** and **total road distance** for the resulting route.\n\nRoutes are based on actual road networks, not straight-line distance.\n\n## Project layout\n\n| File | Purpose |\n|---|---|\n| `main.py` | FastAPI HTTP service (Docker entrypoint) |\n| `route_optimizer.py` | Core optimizer (`optimize_route`) used by the API |\n| `examples/` | Sample HTTP request/response JSON payloads |\n| `utils/` | Optional CLI, benchmarks, multi-route helpers, and sample data (not shipped in Docker) |\n| `utils/samples/` | Example CLI input files (`route.json`, `routes.json`) |\n| `tests/test_route_optimizer.py` | Core optimizer unit tests |\n| `tests/test_main.py` | FastAPI endpoint unit tests |\n| `tests/test_live_integration.py` | Live OpenRouteService integration tests |\n| `pytest.ini` | Pytest config (unit tests run by default) |\n| `Dockerfile` | Container image for the HTTP API |\n| `.env` | OpenRouteService API key for CLI (not committed) |\n| `.env.example` | Example env file for CLI setup |\n\n## Requirements\n\n- Python 3.10+\n- A free [OpenRouteService API key](https://openrouteservice.org/dev/#/signup)\n\n## Installation\n\n```bash\ngit clone https://github.com/elkingarcia11/route-optimizer.git\ncd route-optimizer\n\npython3 -m venv .venv\nsource .venv/bin/activate   # Windows: .venv\\Scripts\\activate\npip install -r requirements.txt\n```\n\n## API key\n\n### HTTP API\n\n`apiKey` is **required in every** `POST /optimize` request body:\n\n```json\n{\n  \"apiKey\": \"your_openrouteservice_api_key\",\n  \"start\": { ... },\n  \"end\": { ... },\n  \"stops\": [ ... ]\n}\n```\n\nThe HTTP API does **not** read `ORS_API_KEY` from the environment.\n\n### CLI and local development\n\n```bash\ncp .env.example .env\n```\n\nEdit `.env`:\n\n```\nORS_API_KEY=your_actual_api_key\n```\n\nThe CLI loads `.env` automatically and uses `ORS_API_KEY` when `--api-key` is not passed.\n\n---\n\n## HTTP API (Docker)\n\nBuild and run on port 8000:\n\n```bash\ndocker build -t route-optimizer .\ndocker run --rm -p 8000:8000 --name route-optimizer route-optimizer\n```\n\nThe image uses `requirements-docker.txt` (runtime deps only — no pytest/httpx).\n\nOn a shared Docker network, other services reach it at:\n\n```\nhttp://route-optimizer:8000/optimize\n```\n\n| Endpoint | Method | Description |\n|---|---|---|\n| `/health` | GET | Health check |\n| `/optimize` | POST | Optimize one route (start → stops → end) |\n| `/docs` | GET | Swagger UI (interactive API docs) |\n| `/redoc` | GET | ReDoc API reference |\n| `/openapi.json` | GET | OpenAPI 3 schema |\n\nSample request/response JSON files live in [`examples/`](examples/).\n\n### Quick start with curl\n\nHealth check:\n\n```bash\ncurl http://localhost:8000/health\n```\n\nOptimize route (see [`examples/optimize.request.json`](examples/optimize.request.json)):\n\n```bash\ncurl -X POST http://localhost:8000/optimize \\\n  -H \"Content-Type: application/json\" \\\n  -d @examples/optimize.request.json\n```\n\nReplace `your_openrouteservice_api_key` in the example file with your real key before running.\n\nExpected response shape: [`examples/optimize.response.json`](examples/optimize.response.json).\n\n### Address schema\n\nThe HTTP API uses the same shape as Go `core.Address`:\n\n| Field | Type | Description |\n|---|---|---|\n| `address1` | string | Primary street address |\n| `address2` | string | Secondary street address |\n| `apartment` | string | Apartment or unit |\n| `city` | string | City |\n| `country` | string | Country |\n| `state` | string | State or province |\n| `zipcode` | string | Postal code |\n| `location` | object | GeoJSON point (`type`, `coordinates`) |\n| `location.coordinates` | `[number, number]` | `[longitude, latitude]` |\n| `verification` | object | Optional pass-through metadata (not used for routing) |\n| `verification.is_verified` | boolean | Whether the address is verified upstream |\n| `verification.verified_at` | string | Verification timestamp (ISO-8601) |\n\nResponse addresses include all input fields plus `routeOrder` (1-based visit position: start = 1, stops follow, end = last).\n\n### Request — `POST /optimize`\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `apiKey` | string | yes | OpenRouteService API key |\n| `start` | Address | yes | Route start |\n| `end` | Address | yes | Route end |\n| `stops` | Address[] | yes | At least **two** stop addresses |\n\n```json\n{\n  \"apiKey\": \"your_openrouteservice_api_key\",\n  \"start\": {\n    \"address1\": \"2249 Washington Ave\",\n    \"city\": \"Bronx\",\n    \"state\": \"NY\",\n    \"zipcode\": \"10457\",\n    \"location\": {\n      \"type\": \"Point\",\n      \"coordinates\": [-73.89406, 40.854388]\n    }\n  },\n  \"end\": {\n    \"address1\": \"2249 Washington Ave\",\n    \"city\": \"Bronx\",\n    \"state\": \"NY\",\n    \"zipcode\": \"10457\",\n    \"location\": {\n      \"type\": \"Point\",\n      \"coordinates\": [-73.89406, 40.854388]\n    }\n  },\n  \"stops\": [\n    {\n      \"address1\": \"1 Adrian Ave\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"location\": {\n        \"type\": \"Point\",\n        \"coordinates\": [-73.91335, 40.87995]\n      }\n    },\n    {\n      \"address1\": \"125 W 228th St\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"location\": {\n        \"type\": \"Point\",\n        \"coordinates\": [-73.90774, 40.88467]\n      }\n    }\n  ]\n}\n```\n\n`location.coordinates` is GeoJSON order: **`[longitude, latitude]`**.\n\nOptional address fields: `address2`, `apartment`, `country`, and `verification` (`is_verified`, `verified_at`).\n\n### Response\n\n```json\n{\n  \"addresses\": [\n    {\n      \"address1\": \"2249 Washington Ave\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"location\": {\n        \"type\": \"Point\",\n        \"coordinates\": [-73.89406, 40.854388]\n      },\n      \"routeOrder\": 1\n    }\n  ],\n  \"totalDistanceMeters\": 7509,\n  \"totalDurationSeconds\": 892\n}\n```\n\n| Field | Description |\n|---|---|\n| `addresses` | Start, stops, and end in time-optimized visit order |\n| `routeOrder` | 1-based position in the route (map to stop sequence, e.g. `pickup.routeNumber`) |\n| `totalDurationSeconds` | Total drive time in seconds (optimization objective) |\n| `totalDistanceMeters` | Total road distance in meters (reporting) |\n\n### Go client\n\nMap each stop's `routeOrder` to your domain model (e.g. `pickup.routeNumber`). Skip start/end if those are depot-only and not pickups.\n\n```go\nPOST http://route-optimizer:8000/optimize\nContent-Type: application/json\n\n{\n  \"apiKey\": \"...\",\n  \"start\": {...core.Address...},\n  \"end\": {...core.Address...},\n  \"stops\": [{...core.Address...}, ...]\n}\n```\n\nOptional container env vars:\n\n| Variable | Default | Description |\n|---|---|---|\n| `ROUTE_OPTIMIZER_PROFILE` | `driving-car` | ORS travel profile |\n| `ROUTE_OPTIMIZER_TIME_LIMIT` | `5` | OR-Tools solver time limit (seconds) |\n\nRun locally without Docker:\n\n```bash\nuvicorn main:app --host 0.0.0.0 --port 8000\n```\n\nThen open Swagger UI at [http://localhost:8000/docs](http://localhost:8000/docs) to try the API interactively.\n\n---\n\n## CLI usage\n\nCoordinates use **`latitude,longitude`** format.\n\n### Command line\n\n```bash\npython route_optimizer.py \\\n  --start \"40.7128,-74.0060\" \\\n  --stop \"40.7580,-73.9855\" \\\n  --stop \"40.7484,-73.9857\" \\\n  --end \"40.7308,-73.9973\"\n```\n\n### JSON input file\n\nCreate `route.json`. Each location can include address fields for labeling (only `lat`/`lng` are used for routing):\n\n```json\n{\n  \"start\": {\n    \"lat\": 40.8515,\n    \"lng\": -73.8955,\n    \"street_address_1\": \"2249 Washington Ave\",\n    \"city\": \"Bronx\",\n    \"state\": \"NY\",\n    \"zip\": \"\"\n  },\n  \"stops\": [\n    {\n      \"lat\": 40.87995,\n      \"lng\": -73.91335,\n      \"street_address_1\": \"1 Adrian Ave\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"zip\": \"10463\"\n    },\n    {\n      \"lat\": 40.88467,\n      \"lng\": -73.90774,\n      \"street_address_1\": \"125 W 228th St\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"zip\": \"10463\"\n    }\n  ],\n  \"end\": {\n    \"lat\": 40.8515,\n    \"lng\": -73.8955,\n    \"street_address_1\": \"2249 Washington Ave\",\n    \"city\": \"Bronx\",\n    \"state\": \"NY\",\n    \"zip\": \"\"\n  },\n  \"profile\": \"driving-car\",\n  \"time_limit_seconds\": 5\n}\n```\n\nLegacy `[lat, lng]` arrays are still supported for start, end, and stops.\n\n```bash\npython route_optimizer.py --input utils/samples/route.json\n```\n\nOr run the CLI module directly:\n\n```bash\npython -m utils.cli --input utils/samples/route.json\n```\n\n### Python module\n\n```python\nfrom route_optimizer import Location, optimize_route\n\nresult = optimize_route(\n    start=Location(40.8515, -73.8955, street_address_1=\"2249 Washington Ave\", city=\"Bronx\", state=\"NY\"),\n    stops=[\n        Location(40.87995, -73.91335, street_address_1=\"1 Adrian Ave\", city=\"Bronx\", state=\"NY\", zip=\"10463\"),\n        Location(40.88467, -73.90774, street_address_1=\"125 W 228th St\", city=\"Bronx\", state=\"NY\"),\n    ],\n    end=Location(40.8515, -73.8955, street_address_1=\"2249 Washington Ave\", city=\"Bronx\", state=\"NY\"),\n    api_key=\"your_api_key\",\n)\n\nprint(result[\"ordered_locations\"])\nprint(result[\"total_duration_seconds\"])\nprint(result[\"total_distance_meters\"])\n```\n\n### CLI output\n\n```json\n{\n  \"ordered_locations\": [\n    {\n      \"lat\": 40.8515,\n      \"lng\": -73.8955,\n      \"street_address_1\": \"2249 Washington Ave\",\n      \"city\": \"Bronx\",\n      \"state\": \"NY\",\n      \"zip\": \"\",\n      \"label\": \"2249 Washington Ave, Bronx, NY\"\n    }\n  ],\n  \"ordered_coordinates\": [[40.8515, -73.8955], ...],\n  \"ordered_indices\": [0, 2, 1, 3],\n  \"stop_order\": [1, 0],\n  \"total_distance_meters\": 21220,\n  \"total_duration_seconds\": 3840,\n  \"optimization_metric\": \"duration\",\n  \"distance_source\": \"openrouteservice\",\n  \"profile\": \"driving-car\"\n}\n```\n\n### CLI options\n\n| Flag | Default | Description |\n|---|---|---|\n| `--start` | — | Start coordinate as `lat,lng` |\n| `--stop` | — | Stop coordinate (repeat for each stop) |\n| `--end` | — | End coordinate as `lat,lng` |\n| `--input`, `-i` | — | JSON input file |\n| `--api-key` | from `.env` | OpenRouteService API key |\n| `--profile` | `driving-car` | Travel mode |\n| `--time-limit` | `5` | OR-Tools solver time limit (seconds) |\n\n### Travel profiles\n\n- `driving-car` (default)\n- `driving-hgv`\n- `foot-walking`\n- `cycling-regular`\n\nSee the [OpenRouteService docs](https://openrouteservice.org/dev/#/api-docs) for the full list.\n\n## Tests\n\n```bash\nsource .venv/bin/activate      # if using a venv\npip install -r requirements.txt\npytest                         # unit tests only (mocked, no API calls)\npytest -m integration -v       # live OpenRouteService tests (uses .env)\npytest -v                      # all tests\n```\n\nBy default, `pytest.ini` excludes integration tests (`-m \"not integration\"`).\n\n| Test file | Coverage |\n|---|---|\n| `tests/test_route_optimizer.py` | `Location`, distance matrix, route optimization, CLI helpers |\n| `tests/test_main.py` | `/health`, `/optimize` validation and responses |\n| `tests/test_live_integration.py` | Live HTTP + optimizer calls (skipped without `ORS_API_KEY` in `.env`) |\n\nUnit tests mock external services and do not require an API key. Integration tests read `ORS_API_KEY` from `.env` for live OpenRouteService calls and pass it as `apiKey` in HTTP request payloads.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felkingarcia11%2Froute-optimizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felkingarcia11%2Froute-optimizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felkingarcia11%2Froute-optimizer/lists"}