By Alex Rowan
Disclosure: Manta Media, the local SEO agency I work at, built the two Apify Actors linked near the end of this post (Google Maps Leads Scorer and Local SEO Bulk Site Auditor). Every step here also works in your own script, and the code is included.
A Google Maps scrape gives you a long list of businesses: names, phones, websites, ratings. What it doesn't tell you is which of them actually need help with their website or local SEO. If you do outreach for an agency or as a freelancer, emailing all 200 is a waste of everyone's time.
This walkthrough goes from a raw Maps scrape to a short, ranked list with one concrete talking point per business. You can do every step yourself in Python, or use an Actor for the scoring part.
Step 1: Scrape one niche in one city
Run Google Maps Scraper (compass/crawler-google-places) for a single category and location. Keep it to one location per run, as the Actor's own input form recommends.
{
"searchStringsArray": ["plumber"],
"locationQuery": "Beaverton, OR, USA",
"maxCrawledPlacesPerSearch": 50,
"language": "en"
}
Each place in the output dataset includes, among many other fields, title, website, phone, totalScore (rating), reviewsCount, categoryName and city. Those seven fields are all we need. Note the run's dataset ID; you'll pass it to the next step.
Step 2: Decide what "needs help" means
These are the checks we use per business. Each one is cheap: one fetch of the homepage, plus robots.txt and an http:// redirect check.
- No website at all. The clearest lead for a web designer.
- No LocalBusiness structured data. No JSON-LD telling Google the business name, address and phone.
- Title and meta description. A missing meta description, or a title over ~60 characters that gets cut off in results.
- Phone and address on the homepage. Local visitors expect to see them, and Google reads them too.
-
HTTPS. A valid certificate, and
http://redirecting tohttps://. - Reviews vs the local median. 3 reviews means little on its own. Against a group median of 31 (from the sample run below), it's a concrete talking point.
Checking for LocalBusiness JSON-LD takes only a few lines of Python:
import json, requests
from bs4 import BeautifulSoup
LOCAL = {"LocalBusiness", "Plumber", "Dentist", "HVACBusiness", "RoofingContractor"} # extend as needed
def local_schema(url):
html = requests.get(url, timeout=(10, 20), headers={"User-Agent": "my-audit-bot/1.0"}).text
soup = BeautifulSoup(html, "lxml")
for tag in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "")
except ValueError:
continue
for d in (data if isinstance(data, list) else data.get("@graph", [data])):
t = d.get("@type", [])
if set(t if isinstance(t, list) else [t]) & LOCAL:
return {"type": t, "name": d.get("name"), "telephone": d.get("telephone")}
return None
Compare the schema telephone with the phone shown on the page. A mismatch is a real NAP (name, address, phone) problem worth pointing out.
Step 3: Score and tier
Add the signals into one 0-100 opportunity score, where higher means a better prospect. These are the weights the Google Maps Leads Scorer documents in its README:
| Signal | Points |
|---|---|
| SEO gap: 100 minus the site's local SEO score, scaled | up to 35 |
| No LocalBusiness schema | +10 |
| No SSL (or no https redirect) | +8 (+4) |
| No meta description / no title | +5 / +4 |
| No mobile viewport | +5 |
| No phone on the homepage / no street address (on the page or in schema) | +4 / +3 |
| Slow site (over 3 s to respond, or PageSpeed mobile under 50 when enabled) | +5 |
| Reviews below the batch median (scaled by how far below) | up to +15 |
| Rating under 4.0 (under 4.5) | +7 (+3) |
Public contact email found (only when findEmails is on) |
+10 |
| No website | 85 base |
| Website failed to load | 45 base |
Review and rating points are added on top of the base for no-website and site-down leads too. Scores are capped at 100. Tiers: hot is ≥ 50 or no website, warm is ≥ 30, cold is < 30. Tune the weights to whatever you sell. A web designer might weight "no website" and "no viewport" higher; an SEO agency might weight schema and reviews.
Be polite
You're visiting small-business websites, so keep the load trivial:
- Respect robots.txt, and skip pages it disallows.
- Use low concurrency (we use 3) and conservative timeouts (10 s connect / 20 s read).
- Cap the download size per page, and use an identifiable User-Agent.
- Run each site with a hard time limit. One pathological page shouldn't stall the whole batch.
- Only collect emails a business shows publicly on its own site, and follow CAN-SPAM / GDPR / PDPA when you contact anyone.
Results from a real run
Here are real rows from a published sample run of the Scorer on Beaverton, OR plumbers (scored 12 Oct 2026, email search off). You can open the full output yourself: JSON · CSV.
| Business | Reviews | Local SEO score | Opportunity | Tier |
|---|---|---|---|---|
| Precision Plumbing Beaverton | 3 | 51 | 48 | warm |
| Anctil Plumbing Inc | 31 | 68 | 27 | cold |
| Leak Locaters | 185 | 82 | 10 | cold |
Two things stand out. The business with the fewest reviews and the weakest site comes out on top, which is the point of scoring. And a high review count doesn't mean a well-optimised site: in a larger run, Apollo Plumbing, Heating, AC & Electric had 2,014 reviews but a local SEO score of 20 and no LocalBusiness schema, which put it in the hot tier. That's why it's worth scoring the website rather than filtering on reviews alone.
The practical outcome: instead of emailing every place on the scrape, you start with the hot and warm rows and one specific, checkable observation for each.
Doing it without writing the code
Disclosure again: Manta Media built both of these Actors. We packaged this workflow as Google Maps Leads Scorer ($0.008 per scored lead; leads that time out are not charged). Paste the Google Maps Scraper dataset ID into datasetId:
{ "datasetId": "<your Google Maps Scraper dataset ID>", "maxLeads": 200, "findEmails": true }
You get one row per business with opportunityScore, tier, pitchPoints, email, localSeoScore, reviewMedian and seoIssues. The Google Maps fields title, website, phone, totalScore, reviewsCount, categoryName and city are mapped automatically. Sort by opportunityScore and start with the hot tier. If you only need the website checks for a list of URLs, the sister Actor Local SEO Bulk Site Auditor does that for $0.004 per URL.
Either way, the steps above work just as well in your own script. What matters is the order: scrape, check, score, then contact only the top of the list.
If you want the whole setup I use around this (the agent instructions, site templates and pre-send checks), it is in the Lead Site Crew Kit, $39.
Top comments (0)