# MapSweep - Google Maps Grid Scraper & Lead Monitor (`safwan_26daud/mapsweep-google-maps-scraper`) Actor

Full-coverage Google Maps scraper: grid search past the 12000-result cap, change tracking, contact enrichment, lead scoring and review analytics.

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

## Pricing

from $9.00 / 1,000 place scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## MapSweep - Google Maps Grid Scraper & Lead Monitor

MapSweep extracts every business in an area, not just the first 120 search results. It splits the area into a grid, searches each cell, and splits any cell that comes back full until coverage is complete.

### What makes it different

- **Full coverage.** Adaptive grid search past Google's result cap. Give a city name or a GeoJSON polygon.
- **Change tracking.** Re-run on a schedule and get only new, changed or closed places. Each place gets a `changeStatus` (`new`, `changed`, `unchanged`, `closed`) and a `changes` object.
- **Contact enrichment.** Crawls each website for emails, phone numbers and social links, with optional email verification (mail server check and a confidence value).
- **Lead scoring.** A 0-100 score using missing website, review volume, rating, contact availability, unclaimed listing and review activity.
- **Review analytics.** Review velocity (last 30 days), owner reply rate, topic sentiment (price, service, wait, quality, location) and a one-line summary.
- **Filters.** Rating, review count, category, with/without website, skip closed places.
- **Spend cap.** Stops the run when charges reach your limit.

### Input (main fields)

| Field | Purpose |
|---|---|
| `searchStrings` | Business types to search, e.g. `dentist` |
| `location` or `polygon` | City/region name, or a GeoJSON Polygon/MultiPolygon |
| `enableGridSplit`, `startCellKm`, `minCellKm`, `saturationThreshold` | Coverage control |
| `minRating`, `minReviews`, `websiteFilter`, `categories`, `skipClosed` | Filters |
| `outputMode`, `trackingStoreName` | `newOrChangedOnly` needs a tracking name; reuse the same name on every scheduled run |
| `enrichContacts`, `verifyEmails`, `maxPagesPerWebsite` | Website contact enrichment |
| `scrapeReviews`, `maxReviewsPerPlace`, `reviewsSort`, `analyzeReviews` | Reviews and analytics |
| `leadScoring`, `maxSpendUsd`, `proxyConfiguration` | Scoring, cap, proxy |

Example:

```json
{
  "searchStrings": ["dentist"],
  "location": "Johor Bahru, Malaysia",
  "websiteFilter": "withoutWebsite",
  "minReviews": 20,
  "leadScoring": true,
  "outputMode": "newOrChangedOnly",
  "trackingStoreName": "jb-dentists"
}
```

### Output

One dataset row per place: `placeId`, `title`, `categories`, `address`, `location`, `phone`, `website`, `rating`, `reviewsCount`, `openingHours`, `claimed`, plus (when enabled) `emails`, `socials`, `leadScore`, `changeStatus`, `changes`, `reviews`, `reviewVelocity30d`, `ownerReplyRate`, `topicSentiment`, `reviewSummary`. A `RUN_SUMMARY` record in the key-value store lists run counts and estimated spend.

### Pay-per-event pricing (set in Console > Monetization)

| Event name | Suggested price |
|---|---|
| `place-scraped` | $0.009 |
| `contact-enrichment` | $0.006 |
| `review-scraped` | $0.001 |
| `review-analysis` | $0.003 |

The spend cap uses these same prices, so keep them in sync with `PRICES` in `src/pipeline.js`.

### Maintenance note

All Google Maps page selectors are in `SELECTORS` at the top of `src/extract.js` (plus the review field selectors inside `scrapeReviews`). If Google changes its layout and fields come back empty, update them there; nothing else needs to change.

# Actor input Schema

## `searchStrings` (type: `array`):

Business types or keywords to search for, e.g. dentist, coffee shop.

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

City, region or country, e.g. Johor Bahru, Malaysia. Ignored if a custom area is given.

## `polygon` (type: `object`):

Optional GeoJSON Polygon or MultiPolygon. Overrides Location.

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

Two-letter ISO country code used for the Google domain and results, e.g. my.

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

Result language code, e.g. en, ms.

## `enableGridSplit` (type: `boolean`):

Search the area cell by cell and split any cell that hits Google's result cap, so the list is complete.

## `startCellKm` (type: `integer`):

Side length of the first grid cells.

## `minCellKm` (type: `number`):

Cells are not split below this size.

## `saturationThreshold` (type: `integer`):

Google shows about 120 results per search, so a cell near that number is probably incomplete.

## `maxPlaces` (type: `integer`):

Stop after this many places in total, across all search terms.

## `minRating` (type: `number`):

Skip places rated below this value. 0 keeps all.

## `minReviews` (type: `integer`):

Skip places with fewer reviews than this.

## `websiteFilter` (type: `string`):

Keep all places, only those with a website, or only those without one (useful for web agencies).

## `categories` (type: `array`):

Keep only places whose category matches one of these names. Empty keeps all.

## `skipClosed` (type: `boolean`):

Skip places marked permanently or temporarily closed.

## `outputMode` (type: `string`):

Return every place, or only places that are new or changed since the last run with the same tracking name.

## `trackingStoreName` (type: `string`):

Name of the store that remembers previous results. Use the same name on every scheduled run. Required for New or changed only.

## `enrichContacts` (type: `boolean`):

Crawl each place's website for emails, phone numbers and social links.

## `verifyEmails` (type: `boolean`):

Check syntax and mail server records and add a confidence value to each email.

## `maxPagesPerWebsite` (type: `integer`):

Limit on pages crawled per website when enriching.

## `scrapeReviews` (type: `boolean`):

Add reviews to each place.

## `maxReviewsPerPlace` (type: `integer`):

Limit on reviews collected per place.

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

Order in which reviews are collected when review scraping is on.

## `analyzeReviews` (type: `boolean`):

Add review velocity, owner reply rate, topic sentiment and a short summary per place.

## `leadScoring` (type: `boolean`):

Score each place from 0 to 100 using rating, review growth, missing website and contact availability.

## `maxSpendUsd` (type: `number`):

Stop the run when charges reach this amount. 0 means no cap.

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

Proxy settings. Residential proxies are recommended for large runs.

## Actor input object example

```json
{
  "searchStrings": [
    "dentist"
  ],
  "location": "Johor Bahru, Malaysia",
  "countryCode": "",
  "language": "en",
  "enableGridSplit": true,
  "startCellKm": 5,
  "minCellKm": 0.5,
  "saturationThreshold": 100,
  "maxPlaces": 10,
  "minRating": 0,
  "minReviews": 0,
  "websiteFilter": "any",
  "skipClosed": true,
  "outputMode": "all",
  "enrichContacts": false,
  "verifyEmails": false,
  "maxPagesPerWebsite": 5,
  "scrapeReviews": false,
  "maxReviewsPerPlace": 20,
  "reviewsSort": "newest",
  "analyzeReviews": false,
  "leadScoring": false,
  "maxSpendUsd": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per place (name, address, phone, website, rating, leads, changes).

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

Counts, splits and estimated spend for this run.

# 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 = {
    "searchStrings": [
        "dentist"
    ],
    "location": "Johor Bahru, Malaysia",
    "maxPlaces": 10,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("safwan_26daud/mapsweep-google-maps-scraper").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 = {
    "searchStrings": ["dentist"],
    "location": "Johor Bahru, Malaysia",
    "maxPlaces": 10,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("safwan_26daud/mapsweep-google-maps-scraper").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 '{
  "searchStrings": [
    "dentist"
  ],
  "location": "Johor Bahru, Malaysia",
  "maxPlaces": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call safwan_26daud/mapsweep-google-maps-scraper --silent --output-dataset

```

## MCP server setup

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

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/npVdVf4PY8fCZ3InN/builds/DPJ2c1RB9nH2geYu8/openapi.json
