Multi-Stop Route, Geocoder & 2-Opt TSP Distance Optimizer avatar

Multi-Stop Route, Geocoder & 2-Opt TSP Distance Optimizer

Pricing

from $2.00 / 1,000 route stop optimizeds

Go to Apify Store
Multi-Stop Route, Geocoder & 2-Opt TSP Distance Optimizer

Multi-Stop Route, Geocoder & 2-Opt TSP Distance Optimizer

Geocode addresses with OpenStreetMap, order stops with nearest-neighbour + 2-opt on estimated distances (straight line x road factor), apply a shift time budget and export a Google Maps multi-stop link. Not a road-routing engine.

Pricing

from $2.00 / 1,000 route stop optimizeds

Rating

0.0

(0)

Developer

Trung Thanh Tran

Trung Thanh Tran

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Security

Limited permissions

Share

Multi-Stop Route Planner with Nominatim Geocoding and 2-Opt (Apify Actor)

Try it on Apify Store: https://apify.com/thanh81646/apify-multi-stop-route-and-distance-optimizer

An Apify actor in Python (httpx + plain math). It takes an origin, an optional destination and a list of stops (addresses or coordinates), orders the stops to shorten the trip, estimates drive time, marks stops that do not fit in a time budget, and builds a Google Maps directions link for the ordered route. No API key is needed.

Distances are estimates: straight-line (Haversine) distance multiplied by a "road circuity" factor (default 1.28). The actor does not call a routing engine, so it knows nothing about real roads, one-way streets, traffic or turn restrictions.

What it does

  1. Resolves each location in this order:
    • explicit lat/lng fields on the stop (geocode_source: "input_coordinates");
    • an address string that is itself "lat, lng" ("inline_coordinates");
    • a small built-in table of about 16 addresses, mostly the Kansas City demo addresses plus a few US city names ("verified_cache");
    • OpenStreetMap Nominatim search, throttled to about 1 request per second ("osm_nominatim");
    • if all of the above fail, an invented point (see below).
  2. Orders the stops with a nearest-neighbour tour from the origin, then 2-opt swaps (up to 60 passes), keeping the origin and the optional destination fixed. It reports the input-order distance (naive_route_total_miles), the optimized distance and the saving. The priority field is copied to the output but is not used when ordering.
  3. Applies the time budget. It walks the ordered stops, adding drive time at averageSpeedMph plus each stop's service minutes. Stops past maxMinutes are marked Deferred (Over Time Window). The first stop is always scheduled. The return leg to the destination is not counted in the budget.
  4. Builds one Google Maps link (/maps/dir/?api=1) with the scheduled stops as waypoints (Google allows at most 20 intermediate waypoints, so extra ones are dropped from the link).

Geocoding fallback: read this before using the output

When an address cannot be resolved (Nominatim returns nothing, is rate-limited, or the request fails), the actor does not stop. It makes up a coordinate: a point within about ±0.04° (roughly 4 km) of the origin, derived from a SHA-256 hash of the address text. The record is marked geocode_source: "fallback_centroid".

These points are not real locations, so distances, ETAs and the stop order involving them are wrong. If the origin itself cannot be resolved, it falls back around Union Station, Kansas City (the built-in default), which makes the whole route meaningless for any other city. Always check geocode_source and origin_geocode_source, and drop or fix rows with fallback_centroid before you use the route. Supplying lat/lng yourself avoids the problem.

Input

{
"origin": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"destination": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"stops": [
{
"label": "Stop A - River Market",
"address": "20 E 5th St, Kansas City, MO 64106",
"lat": 39.1088,
"lng": -94.5818,
"serviceMinutes": 12
},
{
"label": "Stop B - Country Club Plaza",
"address": "4706 Broadway Blvd, Kansas City, MO 64112",
"lat": 39.0419,
"lng": -94.5908,
"serviceMinutes": 15
}
],
"maxMinutes": 120,
"serviceMinutesPerStop": 12,
"averageSpeedMph": 28,
"roadCircuityPercent": 128
}

stops also accepts plain address strings or "lat, lng" strings. Leave destination empty for a one-way route. Runs on the Apify free tier are capped at 25 stops.

Output record (one per stop)

{
"stop_sequence": 1,
"original_input_index": 5,
"schedule_status": "Scheduled",
"within_time_window": true,
"label": "Stop E - Crossroads District",
"address": "1901 Baltimore Ave, Kansas City, MO 64108",
"latitude": 39.0902,
"longitude": -94.5844,
"geocode_source": "input_coordinates",
"priority": 95,
"leg_distance_miles": 0.44,
"leg_distance_km": 0.71,
"leg_drive_minutes": 0.9,
"service_minutes": 12,
"cumulative_miles": 0.44,
"cumulative_minutes": 12.9,
"origin_address": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"origin_geocode_source": "verified_cache",
"destination_address": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"naive_route_total_miles": 22.86,
"optimized_route_total_miles": 16.41,
"distance_saved_miles": 6.45,
"distance_saved_pct": 28.22,
"google_maps_nav_url": "https://www.google.com/maps/dir/?api=1&origin=Union+Station..."
}

Calling it from Python

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("thanh81646/apify-multi-stop-route-and-distance-optimizer").call(
run_input={
"origin": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"destination": "Union Station, 30 W Pershing Rd, Kansas City, MO 64108",
"stops": [
"20 E 5th St, Kansas City, MO 64106",
"6300 Brookside Plaza, Kansas City, MO 64113",
"1901 Baltimore Ave, Kansas City, MO 64108",
],
"maxMinutes": 120,
}
)
for stop in client.dataset(run["defaultDatasetId"]).iterate_items():
if stop["geocode_source"] == "fallback_centroid":
print("WARNING: address not geocoded:", stop["address"])
print(f"#{stop['stop_sequence']} {stop['label']} ({stop['cumulative_minutes']} min) -> {stop['address']}")

In n8n or Make.com, use the Apify "Run an Actor" node/module with the actor ID thanh81646/apify-multi-stop-route-and-distance-optimizer, pass your addresses into stops, and send google_maps_nav_url to whoever drives the route.

Need a custom version?

If you need extra fields, another website, scheduled runs into Google Sheets, Airtable or a webhook, or an n8n / Make workflow around this actor, email ttrungthanh90@gmail.com with the target site and the fields you need, and I will reply with a fixed quote. You can also message me through my Apify profile.

Author

Trung Thanh Tran: Upwork · GitHub · Apify · Contra

The actor processes addresses and coordinates you supply and queries the public OpenStreetMap Nominatim service (© OpenStreetMap contributors, ODbL) within its usage policy. Google Maps and other product names are trademarks of their owners and are used only to build links or identify them. You are responsible for checking real road conditions and for complying with applicable data protection law (including GDPR and CCPA).