A Python tool for comparing travel distances and optimizing multi-stop routes.
Works out of the box with no API key required — or connect Google Maps / OpenRouteService for road-accurate distances and real travel times.
Available as a desktop app (customtkinter) and a web app (Streamlit — deployable to a public URL in minutes).
- Find Nearest — compare distances from one origin to many destinations, sorted closest-first
- Traveling Salesman (TSP) — find the optimal visit order to minimize total travel distance
- Round-trip mode — TSP can optionally return to the origin
- Interactive map — origin pin, numbered destination pins, and the full route polyline drawn on a live map (desktop: tkintermapview; web: Leaflet/Folium)
- Auto-fit map — after calculation, the map automatically centers and zooms to fit all markers
- Three providers — Google Maps (precise road distances + times), OpenRouteService (free API), or Free (Nominatim) (no API key needed — straight-line haversine distances, labeled as such)
- Per-destination overrides — set a custom transport mode or departure time for each individual stop
- Address validation — geocode all addresses before calculating to catch typos early
- Export results as CSV — download a ranked table of distances and durations
- Import destinations from CSV/TXT — bulk-load addresses; auto-detects
address,destination,indirizzo,destinazionecolumns - History — every successful calculation is saved automatically; load or delete individual runs from the sidebar
- Unit switching — Metric (km) and Imperial (mi) with live conversion
- Transport modes — Driving, Walking, Bicycling, Transit (provider-dependent)
- Departure time — specify a future departure for traffic-aware routing (Google Maps)
- Persistent geocoding cache — lookups are stored in a local SQLite database (
.geo_cache.db) so repeat queries make zero API calls
Desktop-only features:
- Compare & History tab — select two or more runs to compare them side-by-side on the map and in a summary table
- Rename / delete saved runs
- Dark / Light / System theme with live preview
- Map styles — Voyager, Light, Dark, Standard (OSM)
- Tile cache for instant repeat map loads
- Persistent settings (
config.json) — API keys, provider, transport, theme saved locally - Show / hide API key toggle
pip install -r requirements.txt
streamlit run web_app.pyOpens at http://localhost:8501.
- Push this repository to GitHub (already done if you cloned it)
- Go to share.streamlit.io and sign in with GitHub
- Click New app → select your repo, branch
main, fileweb_app.py - Click Deploy — Streamlit Cloud builds the environment and gives you a permanent public URL: https://robertoreale-nearest-destination-finder.streamlit.app
No server to manage, no cost. The app restarts automatically when you push updates to GitHub.
Note: On Streamlit Cloud,
route_history.jsonand.geo_cache.dbare ephemeral (reset on each deployment). For persistent history, connect a database or use a cloud-hosted SQLite alternative.
git clone https://github.com/yourusername/nearest-destination-finder.git
cd nearest-destination-finder
pip install -r requirements.txt
python main.pyNo API key? No problem. Select Free (Nominatim) as the provider — the API key field disappears and the app works immediately using OpenStreetMap geocoding and straight-line (haversine) distances. Results are labeled
(straight-line)to make the approximation clear. Geocoding is sequential (~1 sec per address) due to Nominatim's 1 req/sec rate limit.
- Open Google Cloud Console and create a project
- Enable these APIs:
- Distance Matrix API (Find Nearest mode)
- Directions API (TSP mode)
- Geocoding API (map pins and address validation)
- Create an API key under APIs & Services → Credentials
Google Maps requires a billing account but provides a $200/month free credit, which covers thousands of requests for personal use.
- Sign up at openrouteservice.org
- Generate a free API key (2,000 requests/day on the free plan)
- Select a Provider in the left sidebar (Free, Google Maps, or ORS) and enter an API key if required
- Select Units (Metric or Imperial)
- Choose a Mode and optionally expand Route options (transport, departure time, round-trip)
- Type your Origin address
- Add Destinations — click ➕ to add more, or upload a CSV/TXT file
- (Optional) Expand any destination row to set a custom transport mode or departure time for that stop
- Click ✅ Validate addresses to geocode everything before calculating
- Click 🔍 Calculate — results appear on the right with a map and a CSV export button
- Past runs appear in the History sidebar — load or delete individual entries
| Provider | Driving | Walking | Bicycling | Transit |
|---|---|---|---|---|
| Free (Nominatim) | ✅ | ✅ | ||
| OpenRouteService | ✅ | ✅ | ✅ | |
| Google Maps | ✅ | ✅ | ✅ | ✅ |
- Select a Provider in the sidebar; enter the API key and click Save Settings
- Select a Mode and Transport Mode
- Type your Origin address
- Add Destinations one by one, or click Import CSV to load from a file
- (Optional) Click ⚙ on any destination row to override its transport mode or departure time
- (Optional) Click Validate Addresses to check all entries before calculating
- Click Calculate Routes
- Results appear in the list and on the map; click Export CSV next to Results to save them
| Mode | What it does |
|---|---|
| Find Nearest | Calls the Distance Matrix API. Returns all destinations sorted by distance from origin. |
| Traveling Salesman (TSP) | Calls the Directions / Optimization API with waypoint optimization. Returns the shortest overall route visiting all destinations. |
The importer detects a column named address, destination, indirizzo, or destinazione (case-insensitive). If none is found, it reads one address per line.
address
"Rome, Colosseum"
"Florence, Piazza del Duomo"
"Venice, Piazza San Marco"The Export results as CSV button (web app) and Export Results button (desktop) produce:
| Find Nearest | Traveling Salesman (TSP) |
|---|---|
| Rank, Destination, Distance, Duration | Step, Destination, Distance, Duration |
| — | TOTAL row appended at the bottom |
Nearest-Destination-Finder/
├── main.py # Desktop entry point
├── web_app.py # Streamlit web interface
├── requirements.txt
├── requirements-dev.txt # Dev/test dependencies (pytest, responses, …)
├── .streamlit/
│ └── config.toml # Dark theme for the web app
├── config.json # Desktop local settings — auto-created, not committed
├── route_history.json # Saved route history — auto-created, not committed
├── gui/
│ ├── app_window.py # Desktop main window, layout, calculation flow
│ └── components.py # DestinationList and ResultCard widgets
├── api/
│ ├── maps_engine.py # Google Maps (Distance Matrix + Directions)
│ ├── openroute_engine.py # OpenRouteService (Distance Matrix + VROOM Optimization)
│ └── nominatim_engine.py # Free mode: OSM geocoding + haversine distances
├── utils/
│ ├── config_manager.py # Load / save config.json
│ ├── data_importer.py # CSV import and export (stdlib csv, no pandas)
│ ├── geo_cache.py # Persistent SQLite geocoding cache (.geo_cache.db)
│ ├── history_manager.py # Save / load / rename / delete route history
│ └── logger.py # Rotating file (5 MB × 3) + console logger
└── tests/
├── test_maps_engine.py
├── test_openroute_engine.py
├── test_nominatim_engine.py
├── test_history_manager.py
└── test_components.py
pip install -r requirements-dev.txt
pytest -vAll 23 unit tests should pass. Tests use a dedicated .geo_cache_test.db so the real geocoding cache is never touched.
config.jsonandroute_history.jsonare listed in.gitignore— they store API keys and local data, never commit them- Map tiles are cached in
.map_cache.dband geocoding in.geo_cache.db— auto-generated and excluded from git app.logis written to the project root (rotating, max 5 MB × 3 backups) and excluded from git- Google Maps and ORS geocoding uses
ThreadPoolExecutorfor parallel lookups; Nominatim is sequential (1 req/sec rate limit enforced by a module-level timer) - Desktop GUI updates from background threads use
self.after(0, callback)for thread safety - Failed geocoding lookups are never cached, allowing automatic retries on the next calculation
MIT — see LICENSE for details.