# Google Maps Scraper Email Phone Social (`autoharvestor/google-maps-scraper-email-phone`) Actor

Extract Google Maps contact details. 🔥 $5/1k 🔥 Scrape phones, emails, websites, social media links, addresses, zip codes, ratings, and reviews from Google Maps business listings. Export as JSON or CSV, run via API, schedule runs, or integrate with other tools.

- **URL**: https://apify.com/autoharvestor/google-maps-scraper-email-phone.md
- **Developed by:** [Dipendra KC](https://apify.com/autoharvestor) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Production-Ready Google Maps Scraper (Apify Actor Parity)

A high-throughput, production-ready Google Maps business data scraper built in TypeScript with [Crawlee](https://crawlee.dev/) and [Playwright](https://playwright.dev/).

Replicates and enhances the core functionality of Apify's `compass/crawler-google-places` actor:

- **Bypasses the ~120-result cap** using automated geospatial grid tiling and adaptive 4-way recursive subdivision.
- **Extracts rich structured place data** (IDs, categories, opening hours, review distribution, amenities, images, reviews, and website contact enrichment).
- **Multiple deployment modes**: CLI tool, scheduled cron daemon, containerized worker, or headless Fastify REST API.
- **Anti-blocking & resilience**: Proxy rotation, consent interstitial bypass, captcha detection (`sorry.google.com`), and defensive DOM selectors.
- **Pluggable storage**: Streamed JSON, flattened CSV, NDJSON, and SQLite.

***

### Table of Contents

- [Core Crawl Strategy](#core-crawl-strategy)
- [System Architecture](#system-architecture)
- [Input Configuration Reference](#input-configuration-reference)
- [Output Schema](#output-schema)
- [Installation & Quick Start](#installation--quick-start)
- [CLI Commands](#cli-commands)
- [HTTP API Reference](#http-api-reference)
- [Storage Adapters](#storage-adapters)
- [Anti-Blocking & Proxy Management](#anti-blocking--proxy-management)
- [Docker Deployment](#docker-deployment)
- [Testing](#testing)
- [Legal & Compliance Notice](#legal--compliance-notice)

***

### Core Crawl Strategy

Google Maps caps any single search view at approximately 120 results. To achieve exhaustive regional coverage:

```
[Location String] ──▶ [OSM Nominatim] ──▶ [Bounding Box]
                                                │
                                                ▼
                                    [Geospatial Grid Tiler]
                                                │
                       ┌────────────────────────┴────────────────────────┐
                       ▼                                                 ▼
               [Tile 1: Zoom 14]                                 [Tile 2: Zoom 14]
                       │                                                 │
                       ▼                                                 ▼
             [Scroll Results Feed]                             [Scroll Results Feed]
                       │                                                 │
          ┌────────────┴────────────┐                                    │
    Hit ~120 Cap?             Under Cap?                                 │
          │                         │                                    │
          ▼                         ▼                                    ▼
 [Subdivide into 4x]        [Enqueue Places]                   [Enqueue Places]
  (Zoom 15 Quadrants)       (Global Dedup ID)                 (Global Dedup ID)
```

1. **Geocoding:** Resolves place names (e.g. `"Austin, TX"`) to geographical bounding boxes via OpenStreetMap's Nominatim API.
2. **Spatial Tiling:** Subdivides the bounding box into a grid of overlapping coordinate tiles based on the target zoom level.
3. **Scroll-to-load:** Programmatically scrolls each search feed (`div[role="feed"]`) until reaching the "You've reached the end of the list" marker or hitting `maxCrawledPlacesPerSearch`.
4. **Global ID Deduplication:** Extracts place stubs and enqueues detail targets into Crawlee's `RequestQueue` with unique keys based on Google internal IDs (`!1s0x...`, CID, or Place ID).
5. **Adaptive Recursive Subdivision:** When a tile yields >= 115 results, it automatically branches into 4 smaller sub-quadrants with increased zoom level (up to `maxTileDepth`), ensuring high-density areas are fully extracted.

***

### Input Configuration Reference

Configurable via a JSON file, environment variables, or CLI arguments:

| Field | Type | Default | Description |
|---|---|---|---|
| `category` | `string` | `undefined` | Primary business category or keyword (e.g. `"Roofing contractor"`, `"Dentist"`) |
| `location` | `string` | `undefined` | City, metro, or address (e.g. `"Austin, TX"`, `"London, UK"`) |
| `searchTerms` | `string[]` | `[]` | Search queries or multiple keywords (e.g. `["roofing contractors in Austin"]`) |
| `locationQuery` | `string` | `undefined` | Free-text location alias |
| `startUrls` | `string[]` | `[]` | Direct search or place detail public Google Maps URLs |
| `placeIds` | `string[]` | `[]` | Direct Google Place IDs to scrape directly |
| `categories` | `string[]` | `[]` | Filter places post-scrape to keep only matching categories |
| `maxCrawledPlacesPerSearch` | `number` | `100` | Place limit per search tile |
| `maxTotalPlaces` | `number` | `undefined` | Global cap across the entire crawl run |
| `scrapeContactEnrichment` | `boolean` | `true` | Visits place website to extract emails, phone, and social links |
| `maxReviews` | `number` | `5` | Number of recent reviews to scrape per place (`0` = skip) |
| `reviewsSort` | `string` | `"mostRelevant"` | Sort order: `newest`, `mostRelevant`, `highestRating`, `lowestRating` |
| `zoom` | `number` | `14` | Map zoom level for grid generation (1-21) |
| `language` | `string` | `"en"` | Language code for Google Maps (`hl` param) |
| `scrapePlaceDetailPage` | `boolean` | `true` | When false, exports stubs from feed without visiting detail page |
| `maxImages` | `number` | `10` | Max high-resolution photo URLs to extract |
| `includeReviewerInfo` | `boolean` | `false` | Opt-in for reviewer names/profiles (Privacy compliance) |
| `proxyConfig` | `object` | `{ urls: [] }` | List of proxy URLs (`http://user:pass@host:port`) |
| `concurrency` | `number` | `3` | Parallel Playwright browser workers |
| `outputFormat` | `string` | `"json"` | Export format: `json`, `csv`, `ndjson`, `sqlite` |
| `exportDir` | `string` | `"./results"` | Target directory for generated output files |
| `maxTileDepth` | `number` | `2` | Max recursion depth for adaptive tile subdivision |

***

### Output Schema (CRM-Ready Business Lead)

Every scraped business is returned as a structured, CRM-ready lead record:

```json
{
  "business_name": "Example Roofing Co.",
  "category": "Roofing contractor",
  "full_address": "123 Main Street, Austin, TX 78701, USA",
  "phone": "+1 512 555 0100",
  "website": "https://example-roofing.com",
  "email": "info@example-roofing.com",
  "rating": 4.6,
  "review_count": 84,
  "reviews": [
    {
      "author": "A. Smith",
      "rating": 5,
      "text": "Quick estimate and clear communication.",
      "date": "2 weeks ago",
      "response_from_owner": null
    }
  ],
  "lead_score": 90,
  "lead_priority": "high",
  "contactability": "email",
  "opportunity_signals": [
    "no_social_profile_detected"
  ],
  "enrichment_status": "completed",
  "data_source": "google_maps",
  "latitude": 30.2672,
  "longitude": -97.7431,
  "google_maps_url": "https://www.google.com/maps/place/?q=place_id:ChIJ...",
  "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "opening_hours": {
    "Monday": "9:00 AM - 5:00 PM",
    "Tuesday": "9:00 AM - 5:00 PM",
    "Saturday": "Closed"
  },
  "facebook": null,
  "instagram": null,
  "linkedin": null,
  "twitter": null,
  "scraped_at": "2026-03-31T10:00:00.000Z",
  "source_url": "https://www.google.com/maps/search/roofing+contractors+in+Austin"
}
```

#### Lead Scoring & Prioritization

- **`lead_score` (0-100)**: Quantifies lead quality and readiness for sales outreach based on verified email (+25), direct phone (+20), official website (+20), social presence (+10), rating >= 4.0 (+10), reviews count >= 10 (+10), and operating hours (+5).
- **`lead_priority`**: `"high"` (score >= 70), `"medium"` (40-69), or `"low"` (< 40).
- **`contactability`**: Primary communication channel: `"email"`, `"phone"`, `"website"`, or `"none"`.
- **`opportunity_signals`**: Detects growth signals for agencies and sales teams (`"no_website"`, `"no_email_detected"`, `"no_social_profile_detected"`, `"low_rating"`, `"low_review_count"`, `"missing_phone"`, etc.).

***

### Installation & Quick Start

#### Prerequisites

- Node.js 20+ (Node 22 or 24 recommended)
- npm or pnpm

#### 1. Clone & Install

```bash
git clone <repo-url>
cd "1 Google Maps Scraper"
npm install
npx playwright install chromium
npm run build
```

#### 2. Configure Environment

Copy `.env.example` to `.env`:

```bash
cp .env.example .env
```

Add residential or datacenter proxy endpoints if crawling at scale:

```env
PROXIES=http://user:pass@pr.oxylabs.io:7777,http://user:pass@zproxy.lum-superproxy.io:22225
LOG_LEVEL=info
```

#### 3. Run Your First Crawl

```bash
## Using CLI options directly
node dist/index.js run --term "artisan bakery" --location "Austin, TX" --max 10 --out ./results

## Or using a configuration file
node dist/index.js run --config ./examples/search-location.json
```

***

### CLI Commands

#### 1. `scraper run`

Executes a scraping run.

```bash
node dist/index.js run [options]

Options:
  -c, --config <path>      Path to JSON configuration file
  -o, --out <dir>          Output directory for exported files
  -f, --format <format>    Export format: json, csv, ndjson, sqlite (default: json)
  -t, --term <term>        Search term (e.g. "coffee")
  -l, --location <loc>     Location string (e.g. "Brooklyn, NY")
  -m, --max <number>       Max places per search
  --concurrency <number>   Crawler worker concurrency (default: 3)
  --headless <boolean>     Run headless browser (default: true)
  --dry-run                Validate extractors against local HTML fixtures without network
  --fixtures <dir>         Path to fixture folder (default: ./tests/fixtures)
```

#### 2. `scraper geocode`

Inspects coordinates, bounding box, and grid tile count for any location.

```bash
node dist/index.js geocode "Miami Beach, FL" --zoom 14
```

#### 3. `scraper serve`

Launches the Fastify HTTP REST API.

```bash
node dist/index.js serve --port 3000
```

#### 4. `scraper schedule`

Runs recurring scheduled jobs via cron.

```bash
node dist/index.js schedule --cron "0 0 * * *" --config ./examples/search-location.json
```

***

### HTTP API Reference

When started with `node dist/index.js serve`, the scraper exposes full run lifecycle management:

#### `POST /runs`

Start a crawl run.

```json
POST /runs
Content-Type: application/json

{
  "searchTerms": ["italian restaurant"],
  "locationQuery": "Chicago, IL",
  "maxCrawledPlacesPerSearch": 20,
  "outputFormat": "json"
}
```

**Response (202 Accepted):**

```json
{
  "runId": "run_1726508900",
  "status": "RUNNING",
  "exportDir": "d:/.../results/run_1726508900",
  "message": "Crawl run started successfully"
}
```

#### `GET /runs/:id`

Check real-time progress and telemetry.

```json
{
  "runId": "run_1726508900",
  "status": "RUNNING",
  "metrics": {
    "durationSeconds": 42,
    "tilesProcessed": 3,
    "tilesTotal": 12,
    "placesFound": 36,
    "placesScraped": 34,
    "placesFailed": 0,
    "reviewsScraped": 0,
    "blockedRequests": 0,
    "estimatedProxyCostUsd": 0.051
  }
}
```

#### `GET /runs/:id/results`

Download all scraped place results in JSON or streaming NDJSON.

#### `DELETE /runs/:id`

Abort a currently running crawl immediately.

***

### Storage Adapters

The scraper writes incrementally ("write-as-you-go") so progress is never lost on crash or manual abort:

- **JSON (`json`):** Streams newline-delimited records to `places.ndjson` during the crawl, and flushes formatted `places.json` array on completion.
- **CSV (`csv`):** Generates flattened tabular records in `places.csv`. If `maxReviews > 0`, it additionally writes `reviews.csv` linked by foreign key `placeId`.
- **NDJSON (`ndjson`):** High-throughput streaming format for ingestion into ELT/data pipelines (Snowflake, BigQuery, ClickHouse).
- **SQLite (`sqlite`):** Stores normalized records into SQLite database `places.db` with relational `places` and `reviews` tables and indexed `place_id`/`cid`.

***

### Anti-Blocking & Proxy Management

- **Proxy Rotation:** Distributes requests evenly across configured proxy pools. Residential proxies are recommended for Google Maps to avoid datacenter throttling.
- **Stealth Evasions:** Automatically strips `navigator.webdriver`, overrides plugin signatures, randomizes desktop viewports, and injects humanized cursor jitter.
- **Cookie Consent Bypass:** Automatically intercepts and dismisses Google's EU cookie consent redirects (`consent.google.com`).
- **Captcha / Block Detection:** Monitors for `sorry.google.com` redirects or "unusual traffic" block banners. On trigger, it records telemetry, discards the blocked session, and retries with backoff.
- **CI Dry-Run Mode:** Run `node dist/index.js run --dry-run` to validate DOM selectors against offline HTML fixtures in CI without triggering Google bot detection.

***

### Docker Deployment

Build and run in a containerized environment with all Playwright Chromium dependencies pre-installed:

```bash
## 1. Build image
docker build -t google-maps-scraper:latest .

## 2. Run CLI scraper
docker run --rm -v $(pwd)/results:/app/results google-maps-scraper:latest \
  run --term "orthodontist" --location "Denver, CO" --max 20

## 3. Or run as HTTP API Server
docker run -d -p 3000:3000 -v $(pwd)/results:/app/results google-maps-scraper:latest \
  serve --port 3000
```

***

### Testing

```bash
## Run Vitest test suite (extractors, geo tiling, schema validation)
npm test

## Run TypeScript typecheck
npm run typecheck

## Run offline extractor validation
npm run start -- run --dry-run
```

***

### Legal & Compliance Notice

This software is designed to extract publicly accessible information from Google Maps.

- **Terms of Service:** Users are solely responsible for ensuring compliance with Google's Terms of Service and applicable web scraping regulations in their jurisdiction.
- **Personal Data Protection (GDPR / CCPA):** Reviewer personal identifiers (names, profile links) are classified as personal data. Under default settings, this scraper anonymizes reviewer information (`includeReviewerInfo: false`). Reviewer names should only be scraped after explicit opt-in and in compliance with relevant data privacy laws.

# Actor input Schema

## `category` (type: `string`):

Primary business category or niche (e.g. 'Roofing contractor', 'Dentist', 'Coffee shop', 'Chartered accountant').

## `keyword` (type: `string`):

Specific search keyword or trade query (e.g. 'chartered accountant', 'commercial roofing', 'emergency plumber').

## `location` (type: `string`):

City, state, metro area, or address to automatically tile and search (e.g. 'Austin, TX', 'California', 'London, UK').

## `searchTerms` (type: `array`):

Additional search phrases to run in parallel (e.g. \['roof repair', 'commercial roofing']).

## `startUrls` (type: `array`):

Public Google Maps search or place URLs to scrape directly.

## `maxCrawledPlacesPerSearch` (type: `integer`):

Maximum places to extract per search area / tile.

## `maxTotalPlaces` (type: `integer`):

Hard stop cap across all searches and tiles (default 100).

## `scrapeContactEnrichment` (type: `boolean`):

Automatically visits business website to extract contact emails and social media profiles (Facebook, Instagram, LinkedIn, Twitter). Recommended for CRM lead generation.

## `maxReviews` (type: `integer`):

Number of recent customer reviews to extract (author, rating, text, date, owner response).

## `reviewsSort` (type: `string`):

Sort order for extracted reviews.

## `zoom` (type: `integer`):

Zoom level for geospatial tiling (14 recommended for metropolitan business density).

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

Language code for Google Maps results (e.g. 'en', 'es', 'de').

## `scrapePlaceDetailPage` (type: `boolean`):

Scrape complete details including hours, amenities, and photos.

## `includeReviewerInfo` (type: `boolean`):

Include reviewer full names. When disabled, outputs 'Anonymous User' for GDPR/CCPA privacy compliance.

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

Select proxies to be used for Google Maps requests. Apify Residential proxies (groups-RESIDENTIAL) are pre-configured by default to prevent Google rate limits, CAPTCHAs, and 429 blocking.

## `concurrency` (type: `integer`):

Number of parallel browser workers.

## `outputFormat` (type: `string`):

Format for local file export (json, csv, ndjson, sqlite).

## `exportDir` (type: `string`):

Local directory path where scraped results are saved.

## `headless` (type: `boolean`):

Run browser in headless mode (recommended for cloud runs).

## Actor input object example

```json
{
  "category": "Roofing contractor",
  "location": "Austin, TX",
  "maxCrawledPlacesPerSearch": 100,
  "maxTotalPlaces": 100,
  "scrapeContactEnrichment": true,
  "maxReviews": 5,
  "reviewsSort": "mostRelevant",
  "zoom": 14,
  "language": "en",
  "scrapePlaceDetailPage": true,
  "includeReviewerInfo": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "concurrency": 3,
  "outputFormat": "json",
  "exportDir": "./results",
  "headless": true
}
```

# Actor output Schema

## `leads` (type: `string`):

One structured record per local business with name, category, address, phone, website, email, rating, review count, lead score, contactability, and opportunity signals.

## `places` (type: `string`):

Google Maps places dataset records.

## `metrics` (type: `string`):

Execution statistics, runtime durations, tile counts, and estimated proxy costs.

## `summary` (type: `string`):

Overall scraper run output snapshot in JSON.

## `webServer` (type: `string`):

Interactive live-view server and REST API for real-time monitoring.

# 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 = {
    "category": "Roofing contractor",
    "location": "Austin, TX",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("autoharvestor/google-maps-scraper-email-phone").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 = {
    "category": "Roofing contractor",
    "location": "Austin, TX",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("autoharvestor/google-maps-scraper-email-phone").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 '{
  "category": "Roofing contractor",
  "location": "Austin, TX",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call autoharvestor/google-maps-scraper-email-phone --silent --output-dataset

```

## MCP server setup

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

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/UpYHivHAcP4Ef95MX/builds/WST3745smfv5Gwj2j/openapi.json
