When sales operations and field-dispatch teams build outbound or territory pipelines, they almost always run into two surprisingly expensive bottlenecks:
-
Dirty B2B Lead Lists Full of Corporate Chains & Dead Domains: You scrape
2,000healthcare, dental, audiology, or veterinary clinics from a local map directory—only to realize35%of them are disguised franchises or private-equity DSOs/MSOs (Aspen Dental, Heartland Dental, Miracle-Ear, Banfield), and another18%of scraped emails aresentry.wixpress.comtelemetry strings or dead domains. -
Zig-Zag Field Territory Routes: Once you have
15–25verified local prospects in a metro area, ordering visits chronologically or alphabetically wastes25–35%of a rep's shift crisscrossing highways—while enterprise Route Matrix APIs charge steep per-element fees (N × Nmatrix billing).
Both problems can be solved in pure Python 3.11 inside a 256 MB container with zero headless browsers and zero paid third-party API keys.
In this deep dive, we will walk through the exact engineering behind two open-source microservices / Apify Actors I recently shipped:
-
Stage 1: Async HTTP +
DNS/MXContact Enricher & Corporate DSO/Chain Classifier (apify-us-healthcare-clinic-contact-enricher&apify-b2b-lead-and-contact-enricher). -
Stage 2: Road-Calibrated Haversine + Nearest-Neighbor & 2-Opt TSP Multi-Stop Route Optimizer (
apify-multi-stop-route-and-distance-optimizer).
Stage 1: Zero-Browser B2B Contact Enrichment + DNS/MX Verification
Most naive email scrapers run a single regex ([a-zA-Z0-9._%+-]+@...) across raw homepage HTML and call it a day. On modern websites, that immediately pollutes your CRM with three classes of garbage:
-
Asset filename collisions: Retina image naming conventions like
hero-banner@2x.pngorlogo@3x.webp. -
Frontend error-tracking DSNs: Bundled Sentry keys such as
89f2a1@o4504.ingest.sentry.iooruser@sentry-next.wixpress.com. -
Unroutable / parked domains: Legacy domains in outdated footers with no valid
MXorArecords.
Step 1A: Async Domain MX Verification with In-Memory Caching
Instead of paying per-email SMTP verification credits upfront, you can eliminate dead domains in <15 ms (cached) using asynchronous DNS MX lookups via dnspython's dns.asyncresolver, falling back cleanly to A record resolution per RFC 5321 Section 5.1:
import asyncio
import re
import dns.asyncresolver
EMAIL_REGEX = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,12}")
JUNK_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".css", ".js", ".woff", ".woff2")
JUNK_DOMAINS = {"sentry.io", "sentry.wixpress.com", "sentry-next.wixpress.com", "example.com", "wix.com"}
class AsyncMXValidator:
def __init__(self) -> None:
self._domain_cache: dict[str, bool] = {}
async def has_valid_mail_route(self, domain: str) -> bool:
d = domain.strip().lower().rstrip(".")
if not d or "." not in d or d in JUNK_DOMAINS or "sentry" in d:
return False
if d in self._domain_cache:
return self._domain_cache[d]
resolver = dns.asyncresolver.Resolver()
resolver.lifetime = 3.0
deliverable = False
try:
mx_answers = await resolver.resolve(d, "MX")
deliverable = len(mx_answers) > 0
except Exception:
# RFC 5321 fallback: implicit MX via A record if no explicit MX exists
try:
a_answers = await resolver.resolve(d, "A")
deliverable = len(a_answers) > 0
except Exception:
deliverable = False
self._domain_cache[d] = deliverable
return deliverable
async def extract_verified_emails(html: str, validator: AsyncMXValidator) -> list[str]:
candidates: set[str] = set()
for raw in EMAIL_REGEX.findall(html):
em = raw.strip().lower().rstrip(".,;:")
if any(em.endswith(ext) for ext in JUNK_EXTS):
continue
local, _, domain = em.partition("@")
if local and domain and len(local) <= 64:
candidates.add(em)
verified: list[str] = []
for em in sorted(candidates):
if await validator.has_valid_mail_route(em.split("@")[1]):
verified.append(em)
return verified
Because B2B enrichment crawls /, /contact, and /about on the same domain, caching by domain means 90%+ of candidate checks hit _domain_cache in 0.001 ms.
Step 1B: Heuristic Corporate Chain / DSO / Franchise Screening
If you sell medical devices, dental lab services, hearing aids, or practice-management SaaS to independent clinic owners, calling a disguised corporate clinic is a complete waste of SDR dial time—local clinic managers cannot sign software or vendor contracts when procurement is locked at corporate HQ.
Even when a private-equity Dental Support Organization (DSO) or veterinary consolidator keeps a local-sounding brand name ("Oak Creek Family Dental"), they leave four deterministic footprints in the HTML and Schema.org metadata:
-
Corporate Privacy Policy & Script Domains: Footer links pointing to
heartland.com,aspendental.com,banfield.com,vcahospitals.com,miracle-ear.com,beltone.com, orthejoint.com. -
Schema.orgparentOrganization/brandDeclarations: Nested inside<script type="application/ld+json">forMedicalOrganization,Dentist, orVeterinaryCare. -
Franchise / MSO Legal Disclaimers: Regex-detectable regulatory boilerplate in the footer (
"each office is independently owned and operated","backed by ... dental partners"). -
Store-Locator URL Cardinality: Counting unique internal
<a href="/locations/...">subpaths (>= 15distinct location slugs reliably indicates a regional/national multi-site operator rather than a single-doctor private practice).
Combining these four signals into a weighted confidence score (0.0–1.0) lets the pipeline label every enriched clinic cleanly:
-
Verified Independent(score < 0.45): Route straight into outbound CRM sequences. -
Needs Review(0.45 <= score < 0.80): Multi-office local group (e.g.,3–6regional offices—often the highest-value mid-market buyer!). -
Chain Excluded(score >= 0.80): Automatically suppressed whenexcludeChainsFromOutput=True.
When we run apify-us-healthcare-clinic-contact-enricher against a mixed batch (pacificaudiology.com, brooklyndentalstudio.com, banfield.com), it enriches all 3 domains + subpages in ~2.8s / domain inside 26 MB of RSS memory, extracting +1 E.164 phones, verified MX emails, and flagging banfield.com as Chain Excluded (Mars Veterinary Health).
Stage 2: Sub-Second Multi-Stop Route Optimization with 2-Opt TSP
Now suppose your enrichment pipeline outputs 18 verified independent clinics or property leads across Kansas City or Austin for a field representative who only has a 120-minute afternoon window (maxMinutes=120, with 12 minutes on-site dwell time per stop).
Why Road-Adjusted Haversine Beats Paid Matrix APIs for Clustering & Ordering
Calling a commercial Distance Matrix API on 25 stops requires 25 × 25 = 625 matrix elements per run. Across hundreds of daily territory batches, matrix billing adds up fast.
Yet in urban and suburban street networks, empirical transportation research (Ballou et al.) shows that road driving distance correlates tightly with great-circle Haversine distance multiplied by a street-circuity factor (k ≈ 1.25–1.32, with 1.28 as a strong US metro baseline):
import math
def road_adjusted_haversine_miles(
lat1: float, lng1: float, lat2: float, lng2: float, circuity: float = 1.28
) -> float:
"""Computes street-grid adjusted distance in statute miles."""
r_miles = 3958.7613
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lng2 - lng1)
a = math.sin(dp / 2.0) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2.0) ** 2
c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(max(0.0, 1.0 - a)))
return r_miles * c * circuity
Why Nearest-Neighbor Alone Isn't Enough (And How 2-Opt Fixes It)
A greedy Nearest-Neighbor heuristic ("always drive to the closest unvisited stop") is fast (O(N²)), but it is notoriously myopic: early greedy hops leave isolated outlier stops behind, forcing long criss-crossing return legs at the end of the shift (15–25% longer than optimal).
Adding 2-Opt Local Search fixes those crossings in under 2 milliseconds for N ≤ 60 stops!
The geometric intuition behind 2-Opt is simple: take any pair of non-adjacent edges in your tour, reverse the sub-segment tour[i : k + 1] between them, and check if the total route mileage strictly decreases. Repeat until no edge swap improves the tour:
Point = tuple[float, float]
def tour_miles(
pts: list[Point],
order: list[int],
origin: Point,
dest: Point | None,
circuity: float = 1.28,
) -> float:
if not order:
return 0.0
dist = road_adjusted_haversine_miles(*origin, *pts[order[0]], circuity)
for idx in range(len(order) - 1):
dist += road_adjusted_haversine_miles(*pts[order[idx]], *pts[order[idx + 1]], circuity)
if dest is not None:
dist += road_adjusted_haversine_miles(*pts[order[-1]], *dest, circuity)
return dist
def optimize_stops_2opt(
pts: list[Point],
origin: Point,
dest: Point | None = None,
circuity: float = 1.28,
) -> list[int]:
n = len(pts)
if n <= 1:
return list(range(n))
# 1. Greedy Nearest-Neighbor seed tour from fixed origin
unvisited = set(range(n))
tour: list[int] = []
curr = origin
while unvisited:
nxt = min(unvisited, key=lambda i: road_adjusted_haversine_miles(*curr, *pts[i], circuity))
tour.append(nxt)
unvisited.remove(nxt)
curr = pts[nxt]
# 2. Iterative 2-Opt edge-swap refinement (origin & optional dest remain anchored)
best = tour_miles(pts, tour, origin, dest, circuity)
improved = True
while improved:
improved = False
for i in range(n - 1):
for k in range(i + 1, n):
candidate = tour[:i] + list(reversed(tour[i : k + 1])) + tour[k + 1 :]
cand_dist = tour_miles(pts, candidate, origin, dest, circuity)
if cand_dist + 1e-6 < best:
tour, best = candidate, cand_dist
improved = True
return tour
Benchmark: Naive Order vs. Nearest-Neighbor vs. NN + 2-Opt
Testing on realistic metro territory batches (circuity = 1.28, avg_speed = 28 mph, closed round-trip back to depot):
Stop Count (N) |
Unoptimized CRM Order | Nearest-Neighbor Only | Nearest-Neighbor + 2-Opt | Python Solver Compute Time |
|---|---|---|---|---|
6 Metro Stops |
20.48 mi |
18.92 mi |
17.68 mi (-13.7%) |
0.18 ms |
15 Suburban Stops |
68.40 mi |
46.15 mi |
39.80 mi (-41.8%) |
1.42 ms |
30 Territory Stops |
149.20 mi |
84.60 mi |
71.10 mi (-52.3%) |
8.90 ms |
Once optimize_stops_2opt returns the optimal visit indices, enforcing a shift budget (maxMinutes = 120) is a single linear pass accumulating leg_drive_minutes + service_minutes—marking stops that fit inside the window as Scheduled and pushing overflow stops to Deferred for tomorrow's run, then URL-encoding the scheduled addresses directly into a 1-click mobile Google Maps Directions link:
https://www.google.com/maps/dir/?api=1&origin=...&destination=...&travelmode=driving&waypoints=Stop1|Stop2|Stop3
Chaining Both Engines Together in n8n or Make.com
Because both engines follow a clean JSON-in / JSON-out contract, chaining them in n8n, Make.com, or Google Sheets takes four nodes:
- Ingest: Pull raw clinic or business URLs + street addresses from Google Sheets, HubSpot, or a Google Maps scraper.
-
Enrich & Filter Chains: Call
apify-us-healthcare-clinic-contact-enricher(orapify-b2b-lead-and-contact-enricherfor general B2B tech-stack & contact enrichment) withexcludeChainsFromOutput: true. -
Optimize Territory Route: Pass the surviving
Verified Independentaddresses intoapify-multi-stop-route-and-distance-optimizer(or run the Google Apps Script version directly inside Sheets viagoogle_sheets_haversine_route_planner.js). -
Dispatch: Push verified
MXemails to your outreach tool and text the 1-clickgoogle_maps_nav_urlitinerary straight to your field rep's phone.
All repositories linked above are 100% open-source Python & Apps Script implementations, and all six microservices are published on Apify Store (@thanh81646) with synchronous /run-sync-get-dataset-items REST / RapidAPI OpenAPI 3.0 gateways—feel free to clone, benchmark, or wire them directly into your own workflows!
I'm Trung Thanh Tran, a Python, Playwright & AI Workflow Engineer building lean, high-ROI data enrichment and logistics automation tools.
- Want 6 complete Python 3.11 CLI engines + 3 importable n8n/Make workflow blueprints in one instant download? Grab the Python, AI & n8n Scraper Toolkit Pro on Gumroad.
- Looking for a custom B2B lead enrichment engine, Google Sheets / Apps Script route planner, or Apify Actor? Hire me on Upwork or Fiverr.
Top comments (0)