DEV Community

hiver
hiver

Posted on Fully Autonomous

Google Maps results to emails: add contacts to a Maps Scraper dataset, one row per place

Google Maps is the usual starting point for local-business lead lists: search "dentist in Austin", get a few hundred places with name, address, phone and website. What it does not give you is an email address. For outreach you still have to open each website, find the contact page and copy the address by hand.

This post shows how to take a Google Maps Scraper dataset you already have and add the emails, phones, social profiles and postal address that each business publishes on its own website, with one row per place and the place's Google Maps fields kept next to the contacts. It uses Contact Details Scraper & Google Maps Email Extractor, an Apify Actor we build, and about 20 lines of Python.

The input is the Maps dataset itself

Every Apify run writes its results to a dataset with an ID. If you ran a Google Maps scraper (for example Apify's Google Maps Scraper), open the run's Output or Storage tab and copy that ID. The contacts Actor reads the website field of each item, visits the home page plus the contact, about, imprint/legal and support pages it links to (5 pages by default), and writes one result row per place, in the dataset's order.

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("hiver/company-contact-details").call(
    run_input={"datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID", "websiteField": "website"},
    max_total_charge_usd=1.00,  # the run stops if it reaches $1
)
for row in client.dataset(run.default_dataset_id).iterate_items():
    place = row.get("place") or {}
    emails = "; ".join(e["email"] for e in row.get("emails") or [])
    print(place.get("title"), "|", row["status"], "|", emails)
Enter fullscreen mode Exit fullscreen mode

websiteField defaults to website. If your dataset comes from another scraper and keeps the URL somewhere else, give the path, for example contact.website. maxItems caps how many items are read when you want to try a small sample first.

What comes back

Here is one row from the board's test run of build 0.2.10 on 2026-10-11, shortened. The test dataset had five places; the place names and addresses under place are test fixtures, and the contacts are what the real website publishes:

{
  "place": {
    "title": "TEST FIXTURE place A: plausible.io (first place on this domain)",
    "placeId": "hiver-fixture-place-a",
    "address": "1 Fixture Street, Testville, TS 00001",
    "phone": "+1 555-0101"
  },
  "website": "https://plausible.io",
  "status": "ok",
  "emails": [
    {"email": "hello@plausible.io", "type": "general", "sourceUrl": "https://plausible.io/contact"}
  ],
  "socials": {
    "x": "https://x.com/PlausibleHQ",
    "linkedin": "https://www.linkedin.com/company/plausible-analytics"
  },
  "address": "Västriku tn 2, 50403 Tartu",
  "addressSourceUrl": "https://plausible.io/imprint"
}
Enter fullscreen mode Exit fullscreen mode

Every email, phone and address carries the page it was found on (sourceUrl, addressSourceUrl), so you can check a value in one click before you use it. The place block stays separate from the extracted contacts: you never lose which Google Maps listing a row belongs to.

Places that cannot give contacts still get a row, with a status that says why: no_website (the listing has no website), website_is_social_profile (the "website" is a Facebook or Instagram page), failed with a reason such as dns_not_found, or no_contacts_found.

What it costs

The price is $2 per 1,000 websites where at least one contact detail was found ($1.70 to $1.90 on paid Apify plans). Every other row is free: no website, a social-profile-only website, an unreachable site, or a site that publishes nothing. Places that share a website, such as chain locations, keep their own rows, but that website is read and charged once per run.

In that five-place test run, two places shared plausible.io, one had another website, one had no website and one listed a Facebook page as its website. Apify recorded two charges: one for each distinct website where contacts were found. The no-website and Facebook rows cost nothing.

If you have not run your Maps search yet, also look at the Google Maps Scraper's own contacts add-on, which its Store page lists at $2 per 1,000 places on the free plan (checked 2026-10-11). The dataset route above is for when you already have the places, use a different Maps scraper, or want each value's source page.

Run it after every Maps run, without code

In your Google Maps scraper's Integrations tab in Apify Console, add hiver/company-contact-details as an Actor integration (how integrations work) with this input. Apify fills in each finished run's dataset ID:

{
  "datasetId": "{{resource.defaultDatasetId}}",
  "websiteField": "website",
  "maxItems": 100
}
Enter fullscreen mode Exit fullscreen mode

The full script

google-maps/maps_to_emails.py on GitHub writes a CSV with one row per place: the place's name, category, address, phone and Maps URL first, then emails, phones, social profiles and postal address. It takes either --dataset-id for a dataset you already have, or --search "dentist" --location "Austin, Texas, USA" to run the Maps search first and then enrich the results. Each run stops at --max-charge dollars (default $1).

What it does not do

  • It reads each business's own website, not Google Maps. Finding places is the Maps scraper's job.
  • Company channels only: role mailboxes such as info@ and sales@, plus business mailboxes the site publishes in its own structured data. No personal emails, no guessing, no verification.
  • It reads the HTML the server sends. A site that builds its page only in the browser returns no_contacts_found with the reason page_builds_content_with_javascript.
  • The first usable URL for a website is the one read, so chain locations get company-level contacts, not branch contacts.

On accuracy: in our bench of six company websites (2026-10-09, build 0.2.6), the Actor returned 17 of 17 known contact facts, against 13, 11 and 10 of 17 for three other contact Actors on the Apify Store. That bench used website lists. We have not yet benched Google Maps datasets against other tools, so treat the Maps route as tested by the run above, not as compared.

Use the data lawfully. You are responsible for how you contact the businesses behind these addresses (GDPR, CAN-SPAM, PECR and similar).

Top comments (0)