Google Maps Email Cleaner & Lead Verifier
Pricing
from $20.00 / 1,000 clean contacts
Google Maps Email Cleaner & Lead Verifier
Clean, dedupe and verify Google Maps leads from any scraper. Merges duplicate businesses, classifies every email (personal, info@, free, disposable), checks MX records and ranks contacts SAFE/REVIEW/WEAK with a 0-100 priority. CRM-ready. No LLM, no login.
Pricing
from $20.00 / 1,000 clean contacts
Rating
0.0
(0)
Developer
Turgay NANTA
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Share
Google Maps Email & Contact Cleaner: Dedupe, Verify and Prioritize Leads
Scraped Google Maps leads are messy. The same business appears three times, phone numbers come in five formats, half the "emails" are info@ inboxes, and a few are throwaway addresses that will bounce and hurt your sender reputation.
This Actor turns raw Google Maps scraper output into a clean, de-duplicated, CRM-ready contact list and tells you which contacts are safe to email first. Every record gets a best email, an email type (corporate, personal, role inbox, free provider, disposable), a SAFE / REVIEW / WEAK contact-confidence verdict and a 0-100 priority score.
- Works with any Google Maps scraper: connect its dataset, paste JSON, or let the Actor fetch places for you from a search term.
- No LLM, no API keys, no login: fully deterministic rules, so the same input always gives the same output.
- Pay per result: no subscription. See Pricing for worked examples.
Not a scraper. This is the cleaning and qualification layer that runs after the scrape, so you stop paying (in money and in bounces) for rows you should never have emailed.
At a glance
| Input | Raw Google Maps / directory records (records), an Apify dataset ID (datasetId), or search terms (searchTerms) |
| Output | One clean row per unique business + a run summary with a "reach these first" list |
| Key columns | priority, name, phone, best_email, best_email_type, contact_confidence, contact_confidence_reason, website, socials_summary |
| Email checks | Format, role inbox, disposable domain, free provider, personal-name pattern, optional live MX record |
| Dedup | Multi-key merge across sources (website, phone, email), so fragments of the same business become one record |
| Speed | Thousands of records per minute (pure Python, no browser) |
| Runs on | Apify Console, API, schedules, n8n, Make, Zapier, AI agents via MCP |
Quick start (2 minutes)
- Run any Google Maps scraper from Apify Store and copy the dataset ID from its Storage tab.
- Open this Actor, paste the ID into Dataset ID, and click Start.
- Open the Contact Table view in the Dataset tab and sort by
priority.
No data yet? Put a search such as dentist in Austin into Search terms and the Actor fetches places through an upstream Google Maps scraper first, then cleans them.
What one output row looks like
{"priority": 100,"name": "Bay Dental Clinic Downtown","phone": "+14155550142","website": "baydental.com","address": "120 Market St, San Francisco, CA","emails": ["jane.doe@baydental.com", "info@baydental.com"],"best_email": "jane.doe@baydental.com","best_email_type": "corporate","contact_confidence": "SAFE","contact_confidence_reason": "corporate email + complete contact fields","socials_summary": "facebook:facebook.com/baydental · instagram:instagram.com/baydental","email_classes": [{"email": "jane.doe@baydental.com", "type": "corporate", "personal": true, "score": 95},{"email": "info@baydental.com", "type": "role_account", "personal": false, "score": 40}]}
Why people use it
Most Google Maps scrapers are very good at extracting data and do nothing about quality. You get what the listing says, in whatever format it says it. That creates four expensive problems:
- Duplicates. The same clinic appears as "Bay Dental", "Bay Dental Clinic Downtown" and a website-only row from a second source. You email it three times.
- Role inboxes.
info@,contact@,sales@go to a shared desk. They are not useless, but they are not a person, and treating them as one lowers reply rates. - Bad addresses. Disposable or malformed addresses bounce. Bounces damage your domain reputation for every future campaign.
- No priority. A list of 2,000 rows with no ranking means someone has to eyeball it, or you blast everything and hope.
This Actor fixes all four in one pass and gives you a ranked list you can hand to a sales rep or import into a CRM without cleanup.
What it does, step by step
- Reads any shape of input. Field names are auto-detected:
name/title/businessName/company,website/url/domain,phone/phoneUnformatted/phoneNumber,emails/email,socials/social/socialLinks,address/fullAddress,countryCode. You do not need to rename columns. - Normalizes every field.
- Websites become a lowercase canonical domain (
https://www.BayDental.com/contact?utm=xbecomesbaydental.com). - Phones become one consistent format. International fields such as
phoneUnformattedare preferred. When a number has no country code, the record'scountryCodeis used; if the country is unknown, the number is kept in national format and no country code is invented. - Social links are canonicalized and tracking parameters are removed (Facebook, Instagram, X/Twitter, LinkedIn, YouTube, TikTok, Pinterest, Telegram).
- Emails are lowercased, validated and de-duplicated.
- Websites become a lowercase canonical domain (
- Merges duplicates across sources. Records that share any strong key (website, phone, or email) are merged into one business, and missing fields are filled from the other fragments. A website-only row and an email-only row that share a phone number become one complete record. Two different branches that only share a name are not merged.
- Classifies every email. Each address is labeled
corporate,free_provider,role_account,disposableorinvalid, and personal-name addresses (jane.doe@,j_smith@) are detected so a real person ranks above a shared inbox. - Picks the best email per business and gives a contact-confidence verdict with a plain-English reason.
- Scores priority 0-100 from the verdict, email quality and how complete the contact details are.
- Optionally checks MX records for the best email's domain. If the domain has no mail server, a
SAFEverdict is downgraded toREVIEW. - Writes a run summary with verdict counts, email-type counts and a top-10 "reach these first" list.
The contact-confidence verdict
| Verdict | Meaning | What to do |
|---|---|---|
SAFE | Corporate email and at least two other contact fields (phone, name, socials) | Email first |
REVIEW | Role inbox only, free-provider address, or corporate email with thin contact details | Enrich or verify before a cold campaign |
WEAK | No valid email, or disposable address | Call, or skip for email outreach |
Every verdict comes with contact_confidence_reason, for example:
corporate email + complete contact fieldsrole account only (info@/sales@) — find the personfree provider (gmail/hotmail) — corporate confirmation advisedcorporate email but contact fields incompletedisposable (throwaway) emailno valid emailno MX record found (live check)
How the priority score works
The score is transparent and deterministic, so you can explain it to anyone:
| Component | Points |
|---|---|
| Verdict: SAFE / REVIEW / WEAK | 50 / 25 / 5 |
| Best email type: corporate / free provider / role inbox / disposable | 25 / 12 / 6 / 0 |
| Contact completeness: phone, website, address, socials (5 each) | up to 20 |
| Business name present | 5 |
| Maximum | 100 |
Sort by priority and work top-down. The summary's reach_first_list already does this for the top 10.
Use cases
Local lead generation agencies
You scrape "plumbers in Texas" for a client and deliver 3,000 rows. Run this Actor before delivery: duplicates disappear, each business has one best email, and the client receives a ranked list with a clear reason for every verdict. Deliverables look professional and bounce rates drop.
Cold email and SDR teams
Import only SAFE rows into your sequencer for the first wave. Send REVIEW rows to an enrichment step (for example a people-finder) to turn info@ into a named contact. Leave WEAK rows for phone outreach. Your domain reputation stays healthy because disposable and invalid addresses never enter the campaign.
CRM imports and data hygiene
Before importing into HubSpot, Salesforce, Pipedrive or a Google Sheet, normalize phones and domains so your CRM's own dedup rules actually work. Re-run on each new scrape and merge against your existing list by website.
Franchise and multi-location research
Chains appear once per branch with different names. Because merging uses strong keys rather than names, branches with separate websites and phones stay separate, while the same branch scraped twice collapses into one record.
Market mapping and territory planning
Clean data gives honest counts. "How many dental clinics in this city have a reachable corporate email?" is one number in the summary (sendable_SAFE) instead of an afternoon in a spreadsheet.
AI agents and automations
The output is flat, typed and stable, which makes it easy for an AI agent or an n8n / Make workflow to branch on contact_confidence without extra parsing.
Input
Provide one of the three sources. Everything else is optional.
| Field | Type | Description |
|---|---|---|
records | array | Raw business records. Paste a Google Maps scraper's output or your own list. Field names are auto-detected. |
datasetId | string | The ID of an Apify dataset holding raw records, for example the output of a Google Maps scraper run. When set, it is used instead of records (the example records are ignored). |
searchTerms | array of strings | If you have no data yet: the Actor calls an upstream Google Maps scraper with these terms (for example dentist in Berlin), then cleans the result. |
maxPlaces | integer | In search mode, the maximum number of places fetched per term. Default 50. |
liveMxCheck | boolean | Check that the best email's domain has a live MX record. Default off. |
Example: clean an existing dataset
{"datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID","liveMxCheck": true}
Example: paste records from anywhere
{"records": [{"title": "Bay Dental Clinic", "url": "https://www.baydental.com?utm=x","phone": "(415) 555-0142", "phoneUnformatted": "+14155550142", "countryCode": "US","emails": ["info@baydental.com"],"social": ["https://facebook.com/baydental?ref=page_internal"]},{"name": "Bay Dental Clinic Downtown", "website": "baydental.com","phoneNumber": "+1 (415) 555-0142", "email": "jane.doe@baydental.com"}]}
These two rows are the same business. The output is one merged record with both emails, the personal address chosen as best_email, and a SAFE verdict.
Example: start from a search term
{"searchTerms": ["dentist in Austin", "orthodontist in Austin"],"maxPlaces": 100}
Output
Dataset rows (one per unique business)
| Field | Description |
|---|---|
priority | 0-100 score; higher means contact first |
name | Business name (the most complete version across merged fragments) |
website | Canonical domain |
phone | Normalized phone; international format when the country is known |
address | Cleaned address |
emails | All valid emails found, de-duplicated |
email_classes | Per-email label: type, personal, role_account, disposable, free_provider, score |
best_email | The single best address to contact |
best_email_type | corporate, free_provider, role_account, disposable or empty |
contact_confidence | SAFE, REVIEW or WEAK |
contact_confidence_reason | Plain-English reason for the verdict |
mx_verified | Present when liveMxCheck is on: true, false or null (could not check) |
socials | Canonical social profiles as {platform, url} |
socials_summary | One-line readable version for spreadsheets |
The Contact Table view shows the most useful columns in priority order, ready to export as CSV, Excel, JSON or HTML.
Run summary (last row)
{"_summary": {"total_records": 1742,"with_email": 1210,"verdict_distribution": {"SAFE": 486, "REVIEW": 701, "WEAK": 555},"email_type_distribution": {"corporate": 598, "role_account": 402, "free_provider": 196, "disposable": 14, "none": 532},"sendable_SAFE": 486,"reach_first_list": [{"rank": 1, "business": "Bay Dental Clinic Downtown", "priority": 100,"best_email": "jane.doe@baydental.com", "contact_confidence": "SAFE"}]}}
The numbers above are illustrative; your run reports its own.
Pricing
Pay per event, no subscription. Current prices are always shown on the Pricing tab; at the time of writing they are:
| Event | When it is charged | Price |
|---|---|---|
actor-start | Once per run | $0.0001 |
place-processed-100 | Per 100 input records read | $0.30 |
enriched | Per clean, de-duplicated output record | $0.02 |
Duplicates are merged before the per-record charge, so you never pay twice for the same business.
| Input records | Unique businesses after merge (example) | Approximate cost |
|---|---|---|
| 100 | 85 | $0.30 + $1.70 = $2.00 |
| 1,000 | 850 | $3.00 + $17.00 = $20.00 |
| 5,000 | 4,100 | $15.00 + $82.00 = $97.00 |
In search mode the upstream Google Maps scraper bills its own usage separately in your Apify account. If no records are found, nothing is charged by this Actor.
Apify's free plan includes monthly platform credit, which is enough to test this Actor on a real list.
Integrations
Chain it after your Google Maps scraper
In Apify Console open your Maps scraper, go to Integrations, add Run another Actor, pick this Actor and map the default dataset ID to datasetId. Every scrape is then cleaned automatically.
Run it on a schedule
Create a schedule (daily or weekly) for your Maps scraper with the integration above. New businesses appear, duplicates stay merged, and your CRM receives only clean rows.
API
curl -X POST "https://api.apify.com/v2/acts/enezli~maps-contact-cleaner/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"datasetId": "YOUR_DATASET_ID"}'
Python
from apify_client import ApifyClientclient = ApifyClient("YOUR_TOKEN")run = client.actor("enezli/maps-contact-cleaner").call(run_input={"datasetId": "YOUR_DATASET_ID"})safe = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items()if r.get("contact_confidence") == "SAFE"]print(len(safe), "contacts ready to email")
JavaScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('enezli/maps-contact-cleaner').call({ datasetId: 'YOUR_DATASET_ID' });const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items.filter((r) => r.contact_confidence === 'SAFE').length);
n8n, Make, Zapier
Use the official Apify node or app: Run Actor with this Actor, then Get dataset items, then route rows by contact_confidence (for example SAFE to your email tool, REVIEW to an enrichment step, WEAK to a call list).
AI agents (MCP)
This Actor is available through the Apify MCP server, so assistants such as Claude or Cursor can call it with a dataset ID and read back the ranked list.
Tips for best results
- Keep
countryCodein your data. Most Google Maps scrapers include it. It lets the Actor put every phone number in international format. - Turn on
liveMxCheckbefore a cold campaign. It adds a DNS lookup per business and catches dead domains. - Merge multiple sources in one run. Put the output of two different scrapers into
recordstogether; cross-source merging is where the biggest cleanup happens. - Use
REVIEWrows wisely. A role inbox at a small business often reaches the owner. For larger companies, find a named person first. - Re-run instead of editing by hand. The output is deterministic, so re-running on an updated scrape gives consistent results you can diff.
Limitations
- This Actor does not visit websites to discover new emails. It cleans and qualifies the emails your scraper already found. If your scraper has a "contact details" or "website enrichment" option, turn it on for more emails to work with.
- Email checks are rule-based plus optional MX lookup. There is no SMTP mailbox probe, so a corporate address on a valid domain can still bounce if that specific mailbox does not exist.
- The disposable-domain list covers the most common providers, not every one in existence.
- Merging uses website, phone and email. Two records with no shared strong key and different names are kept separate by design, to avoid merging different branches.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Run fails with "No input" | None of records, datasetId, searchTerms was provided | Provide one of the three |
| Few emails in the output | The upstream scraper did not collect emails | Enable the scraper's contact or website enrichment option, then re-run this Actor on the new dataset |
Phones without + | The records have no countryCode and the numbers are in national format | Keep countryCode in the scraper output, or add it to your records |
| Two branches merged into one | They share a website, phone or email in the source data | Check the source; shared central phone lines are a common cause |
mx_verified is null | The DNS lookup timed out | Re-run later; null never downgrades a verdict |
FAQ
Is this a Google Maps scraper? No. It cleans and prioritizes the output of any Google Maps scraper. In search mode it calls an upstream scraper for you, but the value it adds is the cleaning, merging and verdicts.
Which scrapers does it work with? Any scraper or file that produces business records with names, websites, phones or emails. Field names are detected automatically, including the common Google Maps scraper formats.
Does it use AI or send my data to an LLM? No. All logic is deterministic Python. The only optional external lookup is a DNS MX query for email domains.
Will it invent country codes for phone numbers?
No. If a number has no country code and the record has no countryCode, it is kept in national format.
Why is my row marked REVIEW even though it has an email?
Because the best email is a shared inbox (info@), a free provider (gmail.com), or the contact details are thin. The contact_confidence_reason column says which.
Can I export to Excel or Google Sheets? Yes. Use the Dataset tab's export (CSV, Excel, JSON, HTML), or the Google Sheets integration.
Do I pay for duplicates? No. Duplicates are merged first; the per-record charge applies to unique businesses only.
Is the data I send stored? Only in your own Apify storage (dataset and run logs), under your account's retention settings.
Can I use it for GDPR-sensitive outreach? The Actor only processes the data you provide or that a public listing already shows. You remain responsible for having a lawful basis for contacting people in your jurisdiction. Business role inboxes and corporate addresses are flagged separately to help you apply your own policy.
I found a bug or need a field. Open an issue on the Issues tab. Requests with a sample input are fixed fastest.
Trademarks
This Actor is not affiliated with, endorsed by, or connected to Google or Google Maps. All product names are trademarks of their respective owners.