# Multi-Stop Route, Geocoder & 2-Opt TSP Distance Optimizer (`thanh81646/apify-multi-stop-route-and-distance-optimizer`) Actor

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.

- **URL**: https://apify.com/thanh81646/apify-multi-stop-route-and-distance-optimizer.md
- **Developed by:** [Trung Thanh Tran](https://apify.com/thanh81646) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 route stop optimizeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 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

```json
{
  "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)

```json
{
  "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

```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](https://apify.com/thanh81646).

### Author

Trung Thanh Tran: [Upwork](https://www.upwork.com/freelancers/trungthanhtrandev) · [GitHub](https://github.com/Tthanh2k6) · [Apify](https://apify.com/thanh81646) · [Contra](https://contra.com/trung_thanh_tran_7tjnw73h)

### Legal note

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).

# Actor input Schema

## `origin` (type: `string`):

Starting depot, office address, or 'latitude, longitude' coordinate pair.

## `destination` (type: `string`):

Ending depot address or 'latitude, longitude'. Set identical to Start Location for a closed round-trip, or leave empty for an open-ended route.

## `stops` (type: `array`):

Array of stop objects ({label, address, lat, lng, priority, serviceMinutes}) or plain street address strings from Google Sheets, HubSpot, Airtable, or CSV.

## `maxStops` (type: `integer`):

Maximum number of stops to geocode and sequence in a single run (Free-tier Apify runs are capped at 25 results).

## `maxMinutes` (type: `integer`):

Maximum allowed cumulative minutes (driving + on-site dwell time). Stops exceeding this window are marked as Deferred.

## `serviceMinutesPerStop` (type: `integer`):

Default dwell or appointment duration in minutes per stop when not overridden on an individual stop.

## `averageSpeedMph` (type: `integer`):

Average urban/suburban road travel speed in miles per hour used for travel-time estimation.

## `roadCircuityPercent` (type: `integer`):

Multiplier applied to straight-line Haversine distance to approximate real street grids (128 = 1.28x Haversine distance).

## Actor input object example

```json
{
  "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,
      "priority": 90,
      "serviceMinutes": 12
    },
    {
      "label": "Stop B - Country Club Plaza",
      "address": "4706 Broadway Blvd, Kansas City, MO 64112",
      "lat": 39.0419,
      "lng": -94.5908,
      "priority": 100,
      "serviceMinutes": 15
    },
    {
      "label": "Stop C - Brookside",
      "address": "6300 Brookside Plaza, Kansas City, MO 64113",
      "lat": 39.0132,
      "lng": -94.5919,
      "priority": 80,
      "serviceMinutes": 12
    },
    {
      "label": "Stop D - Westport",
      "address": "4050 Pennsylvania Ave, Kansas City, MO 64111",
      "lat": 39.0531,
      "lng": -94.5905,
      "priority": 85,
      "serviceMinutes": 10
    },
    {
      "label": "Stop E - Crossroads District",
      "address": "1901 Baltimore Ave, Kansas City, MO 64108",
      "lat": 39.0902,
      "lng": -94.5844,
      "priority": 95,
      "serviceMinutes": 12
    },
    {
      "label": "Stop F - 18th & Vine",
      "address": "1616 E 18th St, Kansas City, MO 64108",
      "lat": 39.0913,
      "lng": -94.5632,
      "priority": 75,
      "serviceMinutes": 10
    }
  ],
  "maxStops": 10,
  "maxMinutes": 120,
  "serviceMinutesPerStop": 12,
  "averageSpeedMph": 28,
  "roadCircuityPercent": 128
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const 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,
            "priority": 90,
            "serviceMinutes": 12
        },
        {
            "label": "Stop B - Country Club Plaza",
            "address": "4706 Broadway Blvd, Kansas City, MO 64112",
            "lat": 39.0419,
            "lng": -94.5908,
            "priority": 100,
            "serviceMinutes": 15
        },
        {
            "label": "Stop C - Brookside",
            "address": "6300 Brookside Plaza, Kansas City, MO 64113",
            "lat": 39.0132,
            "lng": -94.5919,
            "priority": 80,
            "serviceMinutes": 12
        },
        {
            "label": "Stop D - Westport",
            "address": "4050 Pennsylvania Ave, Kansas City, MO 64111",
            "lat": 39.0531,
            "lng": -94.5905,
            "priority": 85,
            "serviceMinutes": 10
        },
        {
            "label": "Stop E - Crossroads District",
            "address": "1901 Baltimore Ave, Kansas City, MO 64108",
            "lat": 39.0902,
            "lng": -94.5844,
            "priority": 95,
            "serviceMinutes": 12
        },
        {
            "label": "Stop F - 18th & Vine",
            "address": "1616 E 18th St, Kansas City, MO 64108",
            "lat": 39.0913,
            "lng": -94.5632,
            "priority": 75,
            "serviceMinutes": 10
        }
    ],
    "maxStops": 10,
    "maxMinutes": 120,
    "serviceMinutesPerStop": 12,
    "averageSpeedMph": 28,
    "roadCircuityPercent": 128
};

// Run the Actor and wait for it to finish
const run = await client.actor("thanh81646/apify-multi-stop-route-and-distance-optimizer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
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": [
        {
            "label": "Stop A - River Market",
            "address": "20 E 5th St, Kansas City, MO 64106",
            "lat": 39.1088,
            "lng": -94.5818,
            "priority": 90,
            "serviceMinutes": 12,
        },
        {
            "label": "Stop B - Country Club Plaza",
            "address": "4706 Broadway Blvd, Kansas City, MO 64112",
            "lat": 39.0419,
            "lng": -94.5908,
            "priority": 100,
            "serviceMinutes": 15,
        },
        {
            "label": "Stop C - Brookside",
            "address": "6300 Brookside Plaza, Kansas City, MO 64113",
            "lat": 39.0132,
            "lng": -94.5919,
            "priority": 80,
            "serviceMinutes": 12,
        },
        {
            "label": "Stop D - Westport",
            "address": "4050 Pennsylvania Ave, Kansas City, MO 64111",
            "lat": 39.0531,
            "lng": -94.5905,
            "priority": 85,
            "serviceMinutes": 10,
        },
        {
            "label": "Stop E - Crossroads District",
            "address": "1901 Baltimore Ave, Kansas City, MO 64108",
            "lat": 39.0902,
            "lng": -94.5844,
            "priority": 95,
            "serviceMinutes": 12,
        },
        {
            "label": "Stop F - 18th & Vine",
            "address": "1616 E 18th St, Kansas City, MO 64108",
            "lat": 39.0913,
            "lng": -94.5632,
            "priority": 75,
            "serviceMinutes": 10,
        },
    ],
    "maxStops": 10,
    "maxMinutes": 120,
    "serviceMinutesPerStop": 12,
    "averageSpeedMph": 28,
    "roadCircuityPercent": 128,
}

# Run the Actor and wait for it to finish
run = client.actor("thanh81646/apify-multi-stop-route-and-distance-optimizer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "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,
      "priority": 90,
      "serviceMinutes": 12
    },
    {
      "label": "Stop B - Country Club Plaza",
      "address": "4706 Broadway Blvd, Kansas City, MO 64112",
      "lat": 39.0419,
      "lng": -94.5908,
      "priority": 100,
      "serviceMinutes": 15
    },
    {
      "label": "Stop C - Brookside",
      "address": "6300 Brookside Plaza, Kansas City, MO 64113",
      "lat": 39.0132,
      "lng": -94.5919,
      "priority": 80,
      "serviceMinutes": 12
    },
    {
      "label": "Stop D - Westport",
      "address": "4050 Pennsylvania Ave, Kansas City, MO 64111",
      "lat": 39.0531,
      "lng": -94.5905,
      "priority": 85,
      "serviceMinutes": 10
    },
    {
      "label": "Stop E - Crossroads District",
      "address": "1901 Baltimore Ave, Kansas City, MO 64108",
      "lat": 39.0902,
      "lng": -94.5844,
      "priority": 95,
      "serviceMinutes": 12
    },
    {
      "label": "Stop F - 18th & Vine",
      "address": "1616 E 18th St, Kansas City, MO 64108",
      "lat": 39.0913,
      "lng": -94.5632,
      "priority": 75,
      "serviceMinutes": 10
    }
  ],
  "maxStops": 10,
  "maxMinutes": 120,
  "serviceMinutesPerStop": 12,
  "averageSpeedMph": 28,
  "roadCircuityPercent": 128
}' |
apify call thanh81646/apify-multi-stop-route-and-distance-optimizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thanh81646/apify-multi-stop-route-and-distance-optimizer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/39QvsbPHrQ3bEnFcj/builds/c2hC7vVucLpkKgids/openapi.json
