If you do local SEO work, you've run into this: a client's Google Business Profile says one phone number, their website footer says another, and nobody noticed until a customer called the wrong number. Or the postal code on the GBP listing is a leftover from an old office. This kind of drift is called a NAP (Name, Address, Phone) inconsistency, and it's widely cited as a local search ranking factor. But checking it by hand means opening every listing and every website side by side.
I built Google Maps NAP Consistency Checker, an Apify Actor that takes Google Maps scraper output, fetches each business's own website (lightly: homepage plus one likely subpage), and checks whether the name, postal code, and phone number on the Google Business Profile actually show up on the site.
What it does, and what it doesn't
This Actor checks one thing: does a business's own website agree with its Google Business Profile on name, postal code, and phone number. It does not check third-party directories (Yelp, Facebook, etc.). That's a different problem with a different competitor landscape. It does not crawl an entire website; it fetches at most two pages per business (homepage, plus a subpage if one with a keyword like "contact" or "about" is linked from it). It does not use an LLM. It's regex and string matching against fetched text, which makes it fast, cheap, and predictable. There's no model that can hallucinate a match that isn't there.
Businesses with no independent website (only a social profile, or nothing) are skipped entirely, because there's nothing to fetch and compare against.
How it works
For each place with a real website, the Actor:
- Checks
robots.txtfor that domain before fetching anything, and skips the business if the checker's user agent isn't allowed. - Fetches the homepage HTML (up to 3 MB), strips
<script>,<style>, and comments before converting to text, so JavaScript variables and tracking IDs don't get misread as phone numbers. - Looks for an internal link containing a keyword like "contact," "about," "access," or "info" (also handles Japanese equivalents), and fetches that one extra page if found. Mail and tel links and JS pseudo-links are excluded from that search. An early version tried to fetch
mailto:info@...as if it were a page URL, which is exactly the kind of bug you only find on real sites. - Pulls out
href="tel:..."numbers directly (a strong signal), plus any phone-shaped strings in the visible text, covering Japanese formats (03-6434-9090) and US formats ((512) 828-3835,+1 512-828-3835). - Compares postal code and phone number found on the site against what the Google Business Profile has, using the scraper's own
postalCodefield when available, with digit-only and hyphenated variants both checked so formatting differences don't cause false mismatches. - Checks whether a meaningful fraction of the business name's word tokens appear anywhere on the page, as a rough "is this even the right site" sanity check.
Every result also gets a changeStatus: run it again later on the same list and matches that were previously flagged as mismatched but are now fine get tagged FIXED. Useful if you're the one who told the client to fix it and want proof it happened.
Usage
Apify Console
- Run a Google Maps scraper (e.g. Google Maps Scraper) for the businesses you want to audit.
- Open Google Maps NAP Consistency Checker, paste that dataset array into the Google Maps places field.
- Click Start. You're billed per business actually checked. Places with no independent website are skipped for free.
Chaining two Actors with apify-client (Python)
from apify_client import ApifyClient
client = ApifyClient(token="YOUR_APIFY_TOKEN")
# 1. Scrape a set of businesses
scrape_run = client.actor("compass/crawler-google-places").call(
run_input={
"searchStringsArray": ["plumber in Austin, TX"],
"maxCrawledPlacesPerSearch": 100,
}
)
places = list(client.dataset(scrape_run["defaultDatasetId"]).iterate_items())
# 2. Check NAP consistency for each business's own website
nap_run = client.actor("reverenced_garnet/apify-gmaps-nap-checker").call(
run_input={"places": places}
)
results = list(client.dataset(nap_run["defaultDatasetId"]).iterate_items())
mismatches = [r for r in results if r["reasons"]]
print(f"{len(mismatches)} of {len(results)} checked businesses have a NAP issue")
Input
| Field | Type | Description |
|---|---|---|
places |
array (required) | Place objects from a Google Maps scraper's dataset output. Only places with their own (non-social-media) website are checked. The rest are skipped. |
Output
{
"title": "Ultimate Plumber",
"placeId": "ChIJv79rmEfzGGARhXlSEd8ceiQ",
"fetchedOk": true,
"postalMatch": true,
"phoneStatus": "match",
"nameFoundOnSite": true,
"changeStatus": "FIXED",
"reasons": []
}
| Field | Type | Description |
|---|---|---|
title |
string | Business name |
placeId |
string | Google Maps place ID |
fetchedOk |
boolean | Whether the website could be fetched at all (false means every other field is unknown, not "matching") |
postalMatch |
boolean or null | Whether the GBP postal code appears on the site; null if there wasn't enough data on either side to compare |
phoneStatus |
string |
match, mismatch, gbp_missing_on_site_found, site_missing_gbp_has, both_missing, or unknown
|
nameFoundOnSite |
boolean or null | Whether most of the business name's word tokens appear on the page |
changeStatus |
string |
NEW, UPDATED, UNCHANGED, or FIXED versus the previous run on this Actor |
reasons |
array | Plain-English list of what didn't match |
Note that fetchedOk: false and postalMatch: null are deliberately kept separate from a plain "no mismatch found." If the site couldn't be fetched, or there wasn't a postal code to compare on one side, the Actor says so instead of quietly reporting a false match.
Use case: auditing NAP consistency before a local SEO engagement
A common first step for a local SEO agency taking on a new client, or a freelancer building a pitch:
- Scrape the client's own listing (or every location, for a multi-location business) with a Google Maps scraper.
- Run it through this Actor to see whether the phone number and address on the GBP listing actually match the website.
- Use
phoneStatus: gbp_missing_on_site_foundas a quick, low-effort recommendation: "your GBP is missing a phone number your own site already lists, this is a five-minute fix." - Schedule it monthly with Apify's Scheduler for an existing client, and use
changeStatus: FIXEDas evidence the fixes you recommended actually got made.
The same pattern applies to a directory or citation-management tool that wants a quick pre-check on whether a business's own site agrees with itself before flagging bigger discrepancies elsewhere.
A note on scope and responsible use
This Actor fetches at most two pages per business, respects robots.txt for the domain being checked, and identifies itself with its own user agent string. It's not a general-purpose crawler. It only looks at data that's already public on the business's own Google Business Profile and website. It's still worth being explicit that finding a phone number or address on a public page doesn't by itself give you consent to use it for outreach; if you use anything found here for contacting a business, you're responsible for complying with applicable law in your jurisdiction.
Pricing
Pay-per-event: $0.01 when a run starts, plus $0.01 per business actually checked (only businesses that had a website to fetch). Auditing 100 locations costs about $1.01. Places with no independent website are skipped and not charged. Current pricing is always on the Store page.
Try it
The Actor is live on the Apify Store: Google Maps NAP Consistency Checker. I built this as part of a small set of post-processing Actors for Google Maps scraper output. If you're already running a scraper for local SEO or lead work, this is a cheap add-on step to catch a class of client-facing errors that's otherwise easy to miss.
Top comments (0)