# Google Maps Scraper & Email Extractor (no login) (`samurai_hart/google-maps-scraper-honest`) Actor

Scrapes Google Maps places and pulls emails from each business website - no login. Every email gets a confidence label, so a contact-form placeholder like user@gmail.com is never passed off as the business address. You are charged only for emails whose domain matches the business.

- **URL**: https://apify.com/samurai\_hart/google-maps-scraper-honest.md
- **Developed by:** [Samurai Hart](https://apify.com/samurai_hart) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Maps Scraper — it tells you what it could not get

Most scrapers hand you a list and let you assume it is complete. This one doesn't.

Ask for 30 places and Google only has 20? You get this alongside the data:

```json
"requested": 30,
"places_returned": 20,
"pages_fetched": 2,
"not_observed": [
  { "field": "coverage",
    "reason": "20件で新規が出なくなった。Google側の該当がこれ以上無い可能性。全件と読まない" }
]
```

That single field is the point of this Actor. "How do you calculate coverage?" is an open question on the biggest Google Maps scrapers, and an unanswered one. A list of 20 that silently stood in for 30 is how a lead list quietly becomes wrong.

### What you get per place

| Field | Example |
|---|---|
| `name` | WOODBERRY COFFEE 渋谷店 |
| `address` | 〒150-0011 東京都渋谷区東２丁目２０−１８ |
| `phone` | 03-5962-7518 |
| `website` | http://woodberrycoffee.com/ |
| `rating` | 4.4 |
| `categories` | \["コーヒーショップ・喫茶店"] |
| `latitude` / `longitude` | 35.6539429 / 139.7094 |
| `place_id` + `maps_url` | stable identifiers for dedupe |

Measured on a live run: name, address, coordinates, rating and website came back for **100%** of places; phone for **85–100%** depending on the query.

### Emails, with a confidence label on every one

Turn on `extractEmails` and it visits each business website and pulls emails and phone links.

Here is what a plain regex extractor hands you, from four real runs on 2026-08-09:

| String it found | What it actually is |
|---|---|
| `user@gmail.com` | the example text inside a contact form |
| `email@example.com` | the same, on a Brooklyn restaurant |
| `filler@godaddy.com` | left over in a GoDaddy template |
| `hi@typemade.mx` | the agency that built the site's theme |

All four are on the page. None is the business. This Actor labels them `LIKELY_PLACEHOLDER`, still returns them, and refuses to put them in `best_email`. `best_email` is only filled when the email's domain matches the business's own website.

```json
"contacts": {
  "best_email": "hello@ideapeddler.com",
  "best_email_confidence": "DOMAIN_MATCH",
  "emails": [
    { "email": "hello@ideapeddler.com", "confidence": "DOMAIN_MATCH",
      "found_on": "https://ideapeddler.com/contact" }
  ],
  "phones": [],
  "pages_examined": ["https://ideapeddler.com/", "https://ideapeddler.com/contact"]
}
```

#### The hit rate is not the same everywhere, so here it is measured

8 places per query, homepage plus `/contact` only:

| Query | Domain-matching email found |
|---|---|
| `marketing agency in Austin Texas` | 5 of 8 |
| `restaurant in Brooklyn New York` | 4 of 8 |
| `hair salon in London` | 4 of 8 |
| `dentist in Shibuya Tokyo` | **0 of 8** |

Japanese clinics publish a phone number and a form, and no email at all. If someone sells you "emails from Google Maps" as a flat promise, that row is the one they are not showing you. Each record tells you which case it is: a verified email, an unverified candidate, no published email, or a site that could not be read.

#### You are charged for verified emails only

The usual arrangement bills per enrichment *attempt*, so the misses cost the same as the hits. Here the email charge counts only records where the domain actually matched. A site visited with nothing to show for it is free — on the Shibuya dentist run above, the email charge would have been zero.

### Input

```json
{
  "searchQueries": ["marketing agency in Austin Texas"],
  "maxPlacesPerQuery": 30,
  "language": "en",
  "countryCode": "us",
  "extractEmails": true
}
```

Put the location in the query. Works the same for `pizza in Brooklyn New York` or `hair salon in London`.

### What it refuses to do

A dataset that quietly rounds off inconvenient facts is worse than a smaller honest one.

1. **It never presents a short result as a complete one.** Requested versus returned is always in the output, with the reason paging stopped.
2. **It never invents a review count.** Google's search response does not carry one, so `review_count` is `null` with an explicit reason — not `0`.
3. **It never returns an empty list when Google changes format.** If the response no longer matches the expected shape, `response_shape_changed` is `true` and you are told, instead of receiving "no results".
4. **It never reports a missing rating as `0`.** A new place with no reviews and a place rated 0 are different things.
5. **It never logs in.** No cookies, no tokens, no account. Only a public endpoint.
6. **It never calls an unreachable website "no email found".** A site that timed out and a site with no published address are two different records, and they say so.
7. **It never promotes an email it cannot tie to the business.** A `gmail.com` address with the shop's name in it is returned and labelled `FREE_MAIL` — visible to you, but not passed off as verified.

Every place carries its own `not_observed` list, so you can see per-record which fields Google simply does not publish for that business.

### Notes

- Google serves 20 places per page; more are fetched by paging, and results are de-duplicated by `place_id`.
- Opening hours and review counts are not part of this response. They are declared as not observed rather than omitted silently.
- No proxy is required. One is available in the input if your own runs hit rate limits.

# Actor input Schema

## `searchQueries` (type: `array`):

What to search on Google Maps, e.g. \["coffee in Shibuya Tokyo", "dentist in Osaka"]. Include the location in the query.

## `maxPlacesPerQuery` (type: `integer`):

How many places to return per query. Google serves 20 per page; more are fetched by paging. If fewer exist, the run reports the shortfall instead of quietly returning less.

## `language` (type: `string`):

Interface language for the results, e.g. en, ja, de. Affects category names and address formatting.

## `countryCode` (type: `string`):

Country bias for the search, e.g. us, jp, gb.

## `extractEmails` (type: `boolean`):

Visits the official website of each place and extracts emails and phone links. Every email is returned with a confidence label, and a form placeholder like user@gmail.com is never presented as the business address. Slower, because it fetches real websites.

## `contactPagesPerSite` (type: `integer`):

Homepage first, then contact/about pages. Only used when email extraction is on.

## `proxyConfiguration` (type: `object`):

Optional. This Actor queries a public endpoint without cookies or login; a proxy is only needed if your requests get rate limited.

## Actor input object example

```json
{
  "searchQueries": [
    "coffee in Shibuya Tokyo"
  ],
  "maxPlacesPerQuery": 20,
  "language": "en",
  "countryCode": "us",
  "extractEmails": false,
  "contactPagesPerSite": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("samurai_hart/google-maps-scraper-honest").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("samurai_hart/google-maps-scraper-honest").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call samurai_hart/google-maps-scraper-honest --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,samurai_hart/google-maps-scraper-honest"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/O4PuQGmGNvhCx1Usb/builds/eu2lpJXEtcmenls4g/openapi.json
