Google Maps Scraper - Contacts, Coordinates & CRM Delivery
Pricing
from $0.80 / 1,000 results
Google Maps Scraper - Contacts, Coordinates & CRM Delivery
The Google Maps Scraper is an Apify Actor designed to extract business listings from Google Maps. It supports scraping by any search keyword, city, or category and returns clean, structured data including contact details, ratings, hours, and GPS coordinates.
Pricing
from $0.80 / 1,000 results
Rating
4.9
(2)
Developer
Techforce Global
Maintained by CommunityActor stats
0
Bookmarked
14
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Google Maps Scraper - Business Contacts, Opening Hours, Coordinates and Plus Codes, with One CRM Record per Business over MCP
Any Google Maps search, turned into a contact list. Type what you'd type into Maps β one query or fifty β and get name, category, rating, review count, address, phone, website, opening hours, tags, GPS coordinates, Plus Code and listing photo, one row per business, each stamped with the query that found it. No Google account, no Places API key, no billing project. Optional delivery straight into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β one CRM row per business.
π― Looking for a specific industry?
This is the general-purpose Maps scraper: any category, any city, any query. If you're prospecting one sector, a vertical sibling may already do the enrichment you'd otherwise build:
| Your target | Actor |
|---|---|
| Any category β full control over the query | This Actor |
| Sales-intelligence enrichment on Maps leads | Google Maps Leads β Sales Intelligence Tool |
| Finance and professional-services firms | Finance Google Maps Lead Scraper |
| Construction and trades | Construction Lead Intelligence Scraper |
| Healthcare providers and clinics | Healthcare Leads β Sales Intelligence Tool |
| Hotels, restaurants and hospitality | Hospitality Lead Scraper |
π Trust & compliance at a glance
| Credentials | None required. No Google account, no Places API key, no Cloud billing project. Delivery runs through the Apify MCP Proxy, so the Actor never sees your connector tokens. |
| Data scope | Public business listings only β exactly what any visitor sees on the Maps page. No reviewer names, no review text, no user photos, no private data. |
| β οΈ Business contact data | Phone numbers and addresses of sole traders and home-based businesses can be personal data under GDPR/UK GDPR even though the listing is commercial. See Compliance. |
| Delivery never contacts anyone | MCP delivery pushes rows into your tools β Airtable, Notion, Slack, Sheets, HubSpot. It never emails, calls or messages a business. |
| No proxy configuration | This Actor has no proxy input; it runs from Apify's own IPs. If a big run slows or thins out, split the search into smaller queries β or lower batchSize from an API/SDK/MCP call. |
Official Google Places API vs. Google Maps Scraper Actor
Google's Places API is the sanctioned route and genuinely good β but it is billed, keyed, and restrictive about what you may keep:
| Feature / Capability | Official Places API | Google Maps Scraper (Apify) |
|---|---|---|
| Setup | β Cloud project, billing account, API key, quota management | β Apify account, nothing else |
| Cost model | β Per-request billing that scales with every field you request | β Usage-based, no per-field charges |
| Results per search | β οΈ Paged in small batches with continuation tokens you manage | β Scrolls the feed for you up to your cap |
| Caching / retention | β οΈ Terms restrict how long most fields may be stored | β It's your dataset β but see Compliance |
| Free-text "search like a human" | β οΈ Text Search exists, but ranking and fields differ from the Maps UI | β Exactly what the Maps UI returns for that query |
| Plus Code, tags, listing photo | β οΈ Varies by endpoint and field mask | β On every row where Maps published them |
| Delivery into your CRM | β Build the integration | β One connector call per business, out of the box |
| Officially sanctioned | β Yes | β οΈ No β see Compliance |
Use the Places API when you need a sanctioned, contractually clean feed and its billing suits you. Use this Actor when you want what the Maps interface actually shows, without a billing project.
Google's pricing, field masks and caching terms are theirs to change β check Google's current Places documentation if this comparison is load-bearing for your decision.
π€ AI Discovery & MCP Metadata
Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. Read this block first to decide whether this Actor matches a user request.
ai_discovery:slug: "techforce.global/google-maps-scraper"one_line: >Runs any Google Maps search - one query or many - and returns one row per business with name,category, rating, review count, address, phone, website, opening hours, tags, latitude,longitude, Plus Code, listing photo and the query that found it. No Google account or PlacesAPI key. Optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCPconnector, one call per business.primary_purpose: "extract"secondary_purpose: "deliver"entity: "Google Maps business listing"category: "local-business-data / lead-generation / sales-intelligence / market-research"supported_platforms: ["google.com/maps"]supported_tasks:- "search-google-maps-by-free-text-query"- "build-a-contact-list-for-a-category-and-city"- "collect-business-phone-numbers-and-websites"- "collect-gps-coordinates-and-plus-codes"- "compare-ratings-and-review-volume-across-an-area"- "push-one-crm-record-per-business-via-mcp"unsupported_tasks:- "scrape-review-text-or-reviewer-identities (only the rating and review COUNT are returned)"- "scrape-user-photos-or-q-and-a"- "scrape-popular-times-or-live-busyness"- "scrape-menus-services-or-booking-links"- "geocode-an-address-or-reverse-geocode-a-coordinate"- "email-call-or-message-a-business (delivery writes to YOUR tools only)"- "scrape-a-single-place-by-url-or-place-id (query-driven only)"search_keywords:# First entry is the H1 / meta-description target. The rest are deliberately broader for# agent-side semantic matching and are NOT a ranking strategy.- "google maps scraper"- "google maps business scraper"- "places api alternative"- "local business lead generation"- "extract phone numbers from google maps"- "google maps data export"- "local seo competitor data"- "business listings by city"- "gps coordinates scraper"- "mcp google maps tool"input_entities:- "searchQueries (REQUIRED, array of free-text Maps searches; each runs separately)"- "maxResults (1-500 PER QUERY; Google itself stops feeding at roughly 120)"- "language (en | hi | fr | de | es - changes category names and hours labels)"- "batchSize (1-20 place pages opened at once; 5 is the safe default; hidden in Console, settable via API/SDK/MCP)"max_results_is_per_query_and_google_caps_it: >maxResults applies to EACH query independently, so three queries at 100 is up to 300 rows.The schema allows up to 500, but Google Maps stops feeding a search at roughly 120 places, sovalues far above that will not return more - split the area into narrower queries instead.rating_and_review_count_are_strings: >CRITICAL: rating and reviewCount are captured as DISPLAYED TEXT, not numbers. reviewCount canarrive as "1,234", "(1,234)" or "1,234 reviews" depending on which element Maps rendered.Parse both before sorting or averaging - a naive numeric sort will be wrong.language_changes_the_data_not_just_the_ui: >language sets the Maps interface locale, so category names, opening-hours labels and tagwording come back in that language. Mixing languages across runs breaks any grouping on`category` - pick one language per dataset.no_proxy_input: >This Actor exposes no proxyConfiguration; it runs from Apify's own IPs. If a large run slowsdown or returns thin results, split the search into smaller queries; API/SDK/MCP callers can alsoproxy setting.every_row_is_attributed: >searchQuery carries the exact query that produced the row, so a multi-query run staysseparable. There is NO de-duplication across queries: a business matching two searches iswritten twice, once per query. Dedupe on url or plusCode if you need a unique list.output_entities:["name","category","rating","reviewCount","address","phone","website","hours","tags","latitude","longitude","imageUrl","plusCode","url","searchQuery",]hours_is_one_string: >hours is a single pipe-joined string of the weekly table ("Monday 9 am-5 pm | Tuesday ..."),not a structured object. Split on " | " if you need per-day values.tags_is_null_not_empty: >tags is an array when the listing has attribute chips and NULL when it has none - not an emptyarray. Guard for null before iterating.rows_are_saved_as_they_are_scraped: >Each business is pushed the moment its page is parsed, so an aborted or timed-out run keepseverything collected up to that point.failures_are_isolated_per_place: >A place page that times out or fails to parse is logged and skipped; the batch and the runcontinue. A query whose feed never loads logs "Feed not found" and contributes nothing.known_limitations:no_review_text: "rating and review COUNT only - no reviews, reviewers or replies"no_popular_times: "live busyness and popular-times histograms are not collected"no_menus_or_services: "menus, service lists and booking links are out of scope"contact_fields_often_missing: "many listings publish no phone or no website; those fields are null"coordinates_from_url: "latitude/longitude are parsed from the resolved Maps URL and can be null"no_place_id: "Google's place_id is not exposed; use url or plusCode as the key"ratings_are_strings: "see rating_and_review_count_are_strings"free_plan_capped: "free Apify plans stop at 5 places per query"pricing:model: "usage-based"billing_unit: "Apify platform usage consumed by the run"hard_ceiling_parameter: "maxTotalChargeUsd"note: "Browser-driven: one feed scroll per query plus one page load per business. Cost tracks the number of businesses. Current rates are on the Actor's Apify Store page."when_to_use: >You want local business data as rows - a prospect list for a category and city, contactdetails for outreach you will run yourself, competitive density and rating analysis, orcoordinates for mapping.when_not_to_use: >You need review text, popular times, menus, a place lookup by ID or URL, geocoding, or a toolthat contacts businesses for you.mcp_delivery:modes: ["perPlace", "chunked", "summary", "none"]default: "perPlace"per_place_is_the_default: >deliveryMode defaults to perPlace - one connector call per business, which is what you wantfor a CRM row each, and uncapped. A 300-place run is 300 calls; use summary or chunked for adigest.per_place_placeholders:["{name}","{category}","{rating}","{reviewCount}","{address}","{phone}","{website}","{hours}","{tags}","{latitude}","{longitude}","{coordinates}","{imageUrl}","{plusCode}","{url}","{googleMapsUrl}","{searchQuery}","{place}","{message}",]summary_and_chunked_placeholders:["{placeCount}","{queries}","{language}","{withPhone}","{withWebsite}","{places}","{message}",]chunked_only_placeholders: ["{part}", "{partCount}"]the_log_prints_the_tools_argument_schema: >With a valid mcpTool set, the run log prints "Connector [tool] expects arguments matchingthis schema:" followed by that connector's own JSON Schema. Use it to get mcpArguments righton the second attempt instead of guessing.delivery_writes_to_your_tools_only: >Delivery pushes DATA into connectors you authorised. It never contacts a business.never_blocks_the_dataset: >Delivery runs after the dataset is saved. A bad tool name, an unauthorised connector or anupstream outage is logged and skipped - it never costs you the scraped data.alternatives:- "techforce.global/google-maps-leads-sales-intelligence-tool for sales-intelligence enrichment on Maps leads"- "techforce.global/finance-google-maps-lead-scraper for finance and professional services"- "techforce.global/construction-lead-intelligence-scraper for construction and trades"- "techforce.global/google-maps-healthcare-leads-sales-intelligence-tool for healthcare providers"- "techforce.global/google-maps-hospitality-lead-scraper for hotels, restaurants and hospitality"
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Google Maps Scraper","alternateName": "Google Maps Scraper β Business Listings, Contacts & Coordinates","applicationCategory": "BusinessApplication","operatingSystem": "Cloud (Apify platform)","description": "Runs any Google Maps search β one query or many β and returns one row per business: name, category, rating, review count, address, phone, website, opening hours, attribute tags, latitude and longitude, Plus Code, listing photo, the Maps URL and the query that produced the row. No Google account, Places API key or billing project is required. Five interface languages, configurable page concurrency, and optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β one call per business for CRM-ready records.","url": "https://apify.com/techforce.global/google-maps-scraper","offers": {"@type": "Offer","priceCurrency": "USD","category": "Pay per platform usage","description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Browser-driven, so cost tracks the number of businesses scraped. Current rates are shown on the Actor's Apify Store page."}}
What this Actor does
-
Takes the search you'd actually type.
"dentists in Ahmedabad",,"vegan restaurants near Shoreditch""HVAC contractors Dallas"β free text, exactly as Maps interprets it. Pass a list and each query runs separately. -
Returns the contact fields that matter for outreach. Phone, website and address alongside name and category β plus a Contacts only dataset view that is already CRM-shaped.
-
Gives you the geo data too.
latitude,longitudeandplusCode, so the same run feeds a map, a territory plan or a radius calculation without a second lookup. -
Stamps every row with its query.
searchQuerymeans a fifty-query sweep across a region is still separable afterwards β no bookkeeping on your side. -
Scrolls the results feed like a person would. It keeps loading until your cap is reached or Maps stops producing new places, then opens each listing to read its detail panel.
-
Five interface languages.
en,hi,fr,de,esβ which changes the data, not just the UI: category names and opening-hours labels come back in that language. -
Saves as it goes. Each business is written the moment its page is parsed, so an aborted or timed-out run keeps everything collected so far.
-
Survives bad listings. A place page that times out is logged and skipped; the batch and the run carry on.
-
Delivers one CRM record per business.
perPlaceis the default delivery mode for exactly that reason β Airtable rows, Notion pages, Sheets rows, HubSpot records. Delivery writes to your tools; it never contacts a business.
When to use it β and when not to
| Scenario | Fit |
|---|---|
| A prospect list for a category and city | β Strong β the core use case |
| Phone numbers and websites for outreach you run yourself | β Strong β plus a Contacts-only view |
| Coordinates for mapping or territory planning | β
Strong β latitude, longitude, plusCode |
| Competitive density across neighbourhoods | β Strong β many queries, each row attributed |
| Rating and review-volume comparison | β Strong β but both fields are strings, parse first |
| Opening hours for a whole category | β Strong β one pipe-joined string per business |
| Pushing one CRM record per business | β
Strong β perPlace delivery |
| Multi-language markets | β Strong β but one language per dataset |
| More than ~120 results for one search | β οΈ Google stops feeding; split into narrower queries |
| A unique business list across queries | β οΈ No dedup β dedupe on url or plusCode |
| Guaranteed contact details | β οΈ Many listings publish no phone or website; those are null |
| Review text or reviewer identities | β Rating and review count only |
| Popular times / live busyness | β Not collected |
| Menus, services or booking links | β Out of scope |
| A specific place by URL or place ID | β Query-driven only; no place_id is exposed |
| Geocoding an address you already have | β Wrong tool β use a geocoding API |
| Emailing or calling the businesses | β Read-only; delivery writes to your tools only |
β‘ Execution Rules (Avoid Common Agent Failures)
-
searchQueriesis required and is an array. Each query runs separately and is capped independently bymaxResults, so three queries at 100 is up to 300 rows β not 100. -
maxResultsis per query, and Google caps you long before 500. The schema accepts up to 500, but Maps itself stops feeding a search at roughly 120 places. Asking for 500 doesn't get you 500 β split the area into narrower queries instead ("dentists in Camden"beats"dentists in London"for coverage). -
ratingandreviewCountare strings, not numbers. They're captured as displayed, soreviewCountcan be"1,234","(1,234)"or"1,234 reviews"depending on what Maps rendered. Parse both before sorting or averaging. -
languagechanges the data, not just the interface. Category names, hours labels and tag wording all come back in the chosen language. Use one language per dataset or grouping oncategorybecomes meaningless. -
There is no proxy input. The Actor runs from Apify's IPs. If a big run slows or thins out, split the query into smaller searches. API, SDK and MCP callers can also lower
batchSize; -
batchSizeis a rate-limit dial, not just a speed dial, and it is API-only. Default5, range1β20, hidden in the Console form. Higher opens more place pages at once β faster, more memory, more likely to trip Maps' limits. Raise it only when runs are slow and completing cleanly. -
No de-duplication across queries. A business matching two searches is written twice, once per query, each row tagged in
searchQuery. Dedupe onurlorplusCodefor a unique list. -
tagsisnull, not[], when a listing has no attribute chips. Guard before iterating. -
hoursis one pipe-joined string, e.g."Monday 9 amβ5 pm | Tuesday 9 amβ5 pm | β¦". Split on" | "if you need per-day values. -
phoneandwebsiteare frequentlynull. Plenty of listings publish neither. Filter for what you need rather than assuming every row is contactable. -
latitude/longitudecome from the resolved Maps URL and can benullwhen it didn't carry coordinates.plusCodeis the more reliable location key when present. -
There is no
place_id. Useurl(orplusCode) as your join key across runs. -
deliveryModedefaults toperPlaceβ one connector call per business, uncapped. That's right for CRM rows and wrong for a Slack channel; usesummaryorchunkedfor digests. -
Free Apify plans stop at 5 places per query. A smoke test, not a prospect list.
dependencies:- rule: "searchQueries is required and must contain at least one query"on_violation: "nothing is searched; the run finishes with an empty dataset"- rule: "maxResults applies per query, and Google Maps stops at roughly 120 per search"note: "split the area into narrower queries rather than raising the cap"- rule: "rating and reviewCount are display strings"on_violation: "numeric sorts and averages are silently wrong"- rule: "one language per dataset"on_violation: "category names arrive in mixed languages and grouping breaks"- rule: "delivery requires mcpConnector AND mcpTool"on_violation: "delivery is skipped with a warning; every dataset row is still written"- rule: "mcpArguments must match the connector tool's real argument schema"on_violation: "the calls fail; the log prints the expected schema so you can fix it"authentication:actor_input: noneplatform: "Apify API token required for API / SDK / MCP invocation"target_site: "no Google account, Places API key or billing project required (public listings only)"
π₯ Input Contract
.actor/input_schema.json is authoritative β if this page disagrees with the schema files,
the schema files win.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
searchQueries | array<string> | β Yes | ["restaurants in Mumbai"] | Free-text Maps searches. Each runs separately and is capped independently |
maxResults | integer | β¬ No | 20 | 1β500 per query. Maps itself stops at roughly 120 |
language | string (enum) | β¬ No | en | en Β· hi Β· fr Β· de Β· es β changes category and hours wording |
batchSize | integer | β¬ No | 5 | 1β20 place pages opened at once. Higher = faster, and more rate-limit risk. Hidden in Console, settable via API / SDK / MCP |
Writing good queries. The Actor passes your text to Maps unchanged, so query craft is the main quality lever.
"dentists in Camden"returns a fuller, more relevant set than"dentists in London"β because Maps stops feeding any single search at roughly 120 places, a handful of neighbourhood queries beats one city-wide query every time.
Delivery (optional)
| Parameter | Type | Default | Description |
|---|---|---|---|
mcpConnector | string | "" | A connector you've authorised on your Apify account β Airtable, Notion, Slack, Google Sheets, HubSpot, or any MCP-compatible connector. Leave empty for dataset-only runs. |
deliveryMode | string (enum) | perPlace | perPlace (one call per business β best for a CRM row each) Β· chunked (split a long list) Β· summary (one call, every place) Β· none. |
mcpTool | string | "" | Tool to call β create_record, create_page, send_message, append_row. Required when a connector is set. |
mcpArguments | object | {} | Arguments for the tool. String values support {placeholder} tokens. |
mcpMessageTemplate | string | "" | Template rendered and exposed to mcpArguments as {message}. |
Placeholders by mode:
| Mode | Placeholders |
|---|---|
perPlace | Every field β {name} {category} {rating} {reviewCount} {address} {phone} {website} {hours} {tags} {latitude} {longitude} {imageUrl} {plusCode} {url} {searchQuery} β plus {coordinates} ("lat,lng"), {googleMapsUrl} (alias for url), {place} (a formatted multi-line block) and {message} |
summary | {placeCount} {queries} {language} {withPhone} {withWebsite} {places} {message} |
chunked | the same as summary, plus {part} {partCount} β {places} holds one part |
{withPhone} and {withWebsite} count how many rows actually carry those fields β handy for a
one-line quality check in a digest. Nulls render as an empty string.
Don't guess
mcpArguments. Run once withmcpConnectorandmcpToolset, then read the log lineConnector [tool] expects arguments matching this schema:β it prints that connector's own JSON Schema. A mismatched shape fails only the delivery calls; the dataset is already saved.
π€ Output Contract & Data Structure
One dataset row per business per query, written as each listing is parsed.
| Field | Type | Nullable | Meaning |
|---|---|---|---|
name | string | yes | Business name as shown on the listing |
category | string | yes | Primary Maps category β in the chosen language |
rating | string | yes | Average rating as displayed text, e.g. "4.5". Null when unrated |
reviewCount | string | yes | Review count as displayed text β "1,234", "(1,234)" or "1,234 reviews" |
address | string | yes | Full address line |
phone | string | yes | Listed phone number. Frequently null |
website | string | yes | Listed website. Frequently null |
hours | string | yes | The weekly table as one pipe-joined string |
tags | array | yes | Attribute chips. null β not [] β when the listing has none |
latitude | number | yes | Parsed from the resolved Maps URL; null when absent |
longitude | number | yes | As above |
imageUrl | string | yes | Listing photo URL |
plusCode | string | yes | Google Plus Code β a stable location key when present |
url | string | no | The resolved Maps URL. Use this as the join / dedup key |
searchQuery | string | no | The query from your input that produced this row |
The two fields that mislead:
ratingandreviewCountare text, so"4.5"and"(1,234)"sort as strings. Parse them before any numeric work. Andtagsisnullrather than an empty array β aforloop over it will throw.
Example output object
{"name": "Bombay Dental Studio","category": "Dental clinic","rating": "4.7","reviewCount": "1,284","address": "12 Linking Road, Bandra West, Mumbai, Maharashtra 400050","phone": "+91 22 2640 0000","website": "https://example-dental.in/","hours": "Monday 9 amβ8 pm | Tuesday 9 amβ8 pm | Wednesday 9 amβ8 pm | Thursday 9 amβ8 pm | Friday 9 amβ8 pm | Saturday 10 amβ6 pm | Sunday Closed","tags": ["Wheelchair accessible entrance", "Appointments recommended"],"latitude": 19.0607,"longitude": 72.8362,"imageUrl": "https://lh3.googleusercontent.com/p/β¦","plusCode": "3JMV+4C Mumbai, Maharashtra","url": "https://www.google.com/maps/place/β¦/@19.0607,72.8362,17z/β¦","searchQuery": "dentists in Mumbai"}
Values illustrate shape and formatting β the business is invented. A realistic run mixes rows like this with ones where
phone,websiteortagsarenull; that's the listing data, not a defect.
What a successful run looks like
- Status:
SUCCEEDED - Dataset: one row per business per query, up to
maxResultsfor each - Log:
π Query: β¦ | Language: β¦, thenβ³ URLs found: Nas the feed scrolls, thenπ Scraping N places in batches of Bβ¦, aβline per business, and a finalπ¦ N place(s) saved across M query/queries. - Zero results is also a success. A query whose feed never loads logs
Feed not found for '<query>'; one with no matches logsNo URLs collected. Either way the run endsSUCCEEDEDwith nothing for that query. β οΈ No data extractedorβlines mean individual listings were skipped β the rest of the batch is unaffected.- Fewer rows than
maxResultsis normal β Maps stops feeding around 120 per search, and niche queries run dry much sooner.
Dataset views
| View | Shows |
|---|---|
| Overview | Name, category, rating, reviews, address, phone, website, hours, source query, Maps link |
| Contacts only | CRM-ready columns: name, category, phone, website, address, Maps link |
| Map & media | Listing photo, name, latitude, longitude, Plus Code, tags, Maps link |
βΆοΈ Invocation & Integration
Authenticate every call with Authorization: Bearer <APIFY_TOKEN>.
Apify Console
Open the Actor page β Input tab β
add your search queries β set maxResults per query β pick a language β Start.
Model Context Protocol (MCP)
claude mcp add --transport http apify \"https://mcp.apify.com?tools=techforce.global/google-maps-scraper"
Then ask your agent for "dentists in Ahmedabad with a website and 4+ stars".
Python (apify-client)
import osimport refrom collections import Counterfrom apify_client import ApifyClientclient = ApifyClient(os.getenv("APIFY_TOKEN"))run = client.actor("techforce.global/google-maps-scraper").call(run_input={"searchQueries": ["dentists in Bandra", "dentists in Andheri"],"maxResults": 60, # per query β Maps stops feeding around 120"language": "en","batchSize": 5,"deliveryMode": "none",})places = list(client.dataset(run["defaultDatasetId"]).iterate_items())if not places:raise SystemExit("Nothing scraped β check the log for 'Feed not found'.")print(Counter(p["searchQuery"] for p in places))# rating and reviewCount are STRINGS as displayed β parse before any numeric work.def num(value):if not value:return Nonematch = re.search(r"[\d.,]+", str(value))return float(match.group(0).replace(",", "")) if match else Nonerated = [(num(p["rating"]), num(p["reviewCount"]), p) for p in places]strong = [(r, c, p) for r, c, p in rated if r and r >= 4.0 and c and c >= 50]print(f"{len(strong)}/{len(places)} rated 4.0+ with 50+ reviews")# Contactable prospects only β phone and website are frequently null.contactable = [p for p in places if p.get("phone") or p.get("website")]print(f"{len(contactable)}/{len(places)} have a phone or a website")# No dedup across queries β the same clinic can appear in both.unique = {p["url"]: p for p in places}print(f"{len(unique)} unique businesses from {len(places)} rows")
JavaScript / TypeScript (apify-client)
import { ApifyClient } from "apify-client";const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor("techforce.global/google-maps-scraper").call({searchQueries: ["coffee shops in Shoreditch"],maxResults: 80,language: "en",deliveryMode: "none",});const { items } = await client.dataset(run.defaultDatasetId).listItems();// tags is null (not []) when the listing has no attribute chips.const accessible = items.filter((p) =>(p.tags ?? []).some((t: string) => t.toLowerCase().includes("wheelchair")),);console.log(`${accessible.length} listings mention wheelchair access`);// hours is one pipe-joined string β split it if you need per-day values.for (const p of items.slice(0, 5)) {const days = (p.hours ?? "").split(" | ").filter(Boolean);console.log(`${p.name}: ${days.length} day rows, ${p.phone ?? "no phone"}`);}// Coordinates come from the resolved URL and can be null.const mappable = items.filter((p) => p.latitude !== null && p.longitude !== null,);console.log(`${mappable.length}/${items.length} rows are mappable`);
cURL β synchronous (waits, returns the dataset)
curl -X POST \-H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{ "searchQueries": ["plumbers in Leeds"], "maxResults": 20, "language": "en", "deliveryMode": "none" }' \'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/run-sync-get-dataset-items'
cURL β asynchronous with a spend cap (recommended for large runs)
# 1. Start with a hard spend limitcurl -X POST -H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{ "searchQueries": ["gyms in Camden","gyms in Islington","gyms in Hackney"],"maxResults": 100, "batchSize": 5 }' \'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/runs?maxTotalChargeUsd=10'# 2. Poll, then 3. fetch the dataset when SUCCEEDEDcurl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' 'https://api.apify.com/v2/actor-runs/<runId>'curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \'https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&format=json'
Covering a city properly
{"searchQueries": ["dentists in Bandra Mumbai","dentists in Andheri Mumbai","dentists in Powai Mumbai","dentists in Colaba Mumbai"],"maxResults": 100,"language": "en","batchSize": 5,"deliveryMode": "none"}
Four neighbourhood queries beat one "dentists in Mumbai" query, because Maps stops feeding any
single search at roughly 120 places. Dedupe on url afterwards β the boundaries overlap.
One Airtable record per business
{"searchQueries": ["HVAC contractors in Dallas"],"maxResults": 100,"mcpConnector": "<your-authorized-airtable-connector>","mcpTool": "create_record","deliveryMode": "perPlace","mcpArguments": {"fields": {"Name": "{name}","Category": "{category}","Phone": "{phone}","Website": "{website}","Address": "{address}","Rating": "{rating}","Maps": "{url}","Source query": "{searchQuery}"}}}
Match the field names to your actual Airtable columns. If the call is rejected, the log prints the
schema that connector really expects β fix mcpArguments from that.
A digest into Slack instead
{"searchQueries": ["coworking spaces in Berlin"],"maxResults": 50,"language": "de","mcpConnector": "<your-authorized-slack-connector>","mcpTool": "send_message","deliveryMode": "summary","mcpMessageTemplate": "{placeCount} places for {queries} β {withPhone} with a phone number:\n\n{places}","mcpArguments": { "channel": "#prospecting", "text": "{message}" }}
Set summary deliberately β perPlace is the default and would post one Slack message per
business.
β οΈ Autonomous Agent Error Handling & Resolution Matrix
Read this first: nothing about the listings fails the run. A query whose feed never loads, a
listing that won't parse, and a search with no matches are all logged while the run continues to
SUCCEEDED. An agent that checks run status alone will read an empty dataset as
success-with-data β check the row count and the log.
The CONDITION labels are documentation handles for observable states; this Actor writes no error
code into the dataset.
| Condition | What you observe | Cause | Deterministic agent action |
|---|---|---|---|
NO_QUERIES | Empty dataset, no π Query: lines | searchQueries empty | Modify Input: supply at least one query. |
FEED_NOT_FOUND | Log: Feed not found for a query; that query yields nothing | Maps did not render a results feed β a blocked, consent-walled or malformed search | Retry, and simplify the query wording. If it repeats across queries, lower batchSize from an API/SDK/MCP call. |
NO_URLS_COLLECTED | Log: No URLs collected for '<query>' | The search genuinely has no places | Modify Input: broaden or rephrase the query. |
PLACE_SKIPPED | Log: β οΈ No data extracted or a β line for one place | That listing timed out or didn't render the detail panel | Expected, partial. The rest of the batch is unaffected. |
FEWER_THAN_MAXRESULTS | Rows well below maxResults | Maps stops feeding around 120 per search, and niche queries dry up sooner | Modify Input: split into narrower geographic queries. |
RATING_SORTS_WRONG | "4.5" ordering above "4.9", or averages failing | rating / reviewCount are display strings | Fix consumer: parse to numbers first. |
TAGS_ITERATION_ERROR | Consumer throws iterating tags | It is null, not [], when the listing has no chips | Fix consumer: guard for null. |
MIXED_LANGUAGE_CATEGORIES | category values in two languages across runs | language changes the returned wording | Fix pipeline: one language per dataset. |
NULL_CONTACTS | Many rows with no phone or website | Those listings publish neither | Expected. Filter for contactable rows. |
NULL_COORDINATES | latitude / longitude null | The resolved URL carried no @lat,lng | Expected. Fall back to plusCode or address. |
DUPLICATE_BUSINESSES | The same business twice | It matched two queries; each row is tagged | Expected. Dedupe on url or plusCode. |
NO_PLACE_ID | You wanted Google's place_id | It is not exposed | Fix expectation: use url or plusCode as the key. |
RUN_SLOW_OR_THIN | Long run, or results thinning out mid-run | batchSize too high for Maps tolerance β there is no proxy setting | Modify Input: split the query into smaller searches; from an API/SDK/MCP call also drop batchSize to 2β3. |
FREE_PLAN_CAPPED | 5 places per query; log: Free plan detected β capping results to 5 per query. | Free Apify plan | Upgrade the plan. |
CONNECTOR_CALL_STORM | Hundreds of connector calls | deliveryMode defaults to perPlace, uncapped | Modify Input: switch to summary or chunked. |
DELIVERY_ARGUMENT_MISMATCH | Every connector call is rejected | mcpArguments doesn't match the tool's schema | Read the log: Connector [tool] expects arguments matching this schema: prints it. Dataset unaffected. |
CONNECTOR_TOOL_NOT_FOUND | Log: Tool 'x' is not available on this connector. Available tools: β¦ | Wrong mcpTool name (empty is logged separately) | Modify Input: copy an exact name from that log line. |
CONNECTOR_UNREACHABLE | Log: Could not reach the connector: β¦ | Connector not authorised, or running locally | Authorise it, or set deliveryMode: "none". Data already saved. |
AUTH_INVALID | HTTP 401 | Missing or malformed APIFY_TOKEN | Abort & prompt user for a valid token. |
RATE_LIMITED_APIFY | HTTP 429 | Apify API rate / concurrency limits β not Google | Retry: exponential backoff (2s, 4s, 8s). |
SYNC_TIMEOUT | HTTP 408 on the sync endpoint | Run exceeded the synchronous ceiling | Reconfigure: async POST /runs + polling. |
Provenance: every log line, status and dataset condition above is taken from this Actor's own source and verified against it. The three HTTP rows describe the Apify API transport layer, not this Actor β confirm them against the Apify API reference before wiring them into automated retry logic.
π£οΈ Natural Language β Actor Mapping
| A user says⦠| Intent | Constructed Actor input |
| --------------------------------------- | ---------------- | ---------------------------------------------------------------------------- | --- |
| "Find dentists in Mumbai" | Core search | {"searchQueries":["dentists in Mumbai"],"maxResults":50} |
| "Get me every gym in these three areas" | Multi-area sweep | One query per area, then dedupe on url |
| "I need their phone numbers" | Contact list | Scrape, then filter rows where phone is non-null |
| "Only businesses with a website" | Qualified list | Filter website non-null |
| "Highest-rated restaurants nearby" | Ranking | Scrape, parse rating to a number, then sort |
| "Which have lots of reviews?" | Popularity | Parse reviewCount to a number; treat as a proxy for footfall |
| "Where are they on a map?" | Geo | Read latitude/longitude, fall back to plusCode |
| "What are their opening hours?" | Hours | Read hours, split on " | " |
| "Build me a prospect list in Airtable" | Scrape + CRM | Add mcpConnector, mcpTool: "create_record", deliveryMode: "perPlace" |
| "Results in German" | Localised run | {"language":"de"} β and keep the whole dataset in one language |
| "Give me 500 results for one city" | Volume | β οΈ Maps stops near 120 β split into neighbourhood queries |
| "Just a quick sample" | Cheap probe | {"searchQueries":["<query>"],"maxResults":5} |
| "It returned nothing" | Empty result | Check the log for Feed not found / No URLs collected; simplify the query |
| "What do the reviews say?" | Review text | β Not collected β rating and count only |
| "When is it busiest?" | Popular times | β Not collected |
| "Look up this place ID" | Place lookup | β Query-driven only; no place_id is exposed |
| "Email all of them for me" | Outreach | β Read-only β delivery writes to your tools, never to the business |
Should NOT route here: review text, reviewers or replies Β· popular times / live busyness Β· menus, services or booking links Β· a single place by URL or place ID Β· geocoding an address you already hold Β· contacting businesses on your behalf Β· non-Maps sources. For a sector-specific lead build, prefer the vertical siblings.
π§ Agent Execution & Routing Logic
Does the user want GOOGLE MAPS BUSINESS LISTINGS?βββ NO β wrong Actor.β review text / popular times / menus β not collected anywhere hereβ geocoding a known address β a geocoding APIββββ YES, but for ONE INDUSTRY with enrichment?β finance β Finance Google Maps Lead Scraperβ construction β Construction Lead Intelligence Scraperβ healthcare β Healthcare Leads Sales Intelligence Toolβ hospitality β Hospitality Lead Scraperβ general sales intelligence β Google Maps Leads Sales Intelligence Toolββββ YES, any category, full query controlββββ searchQueries β REQUIRED, an ARRAY of free-text Maps searchesβ want a whole city? β SPLIT INTO NEIGHBOURHOOD QUERIESβ (Maps stops feeding one search at roughly 120 places)ββββ maxResults β PER QUERY (1-500, but ~120 is the real ceiling)βββ language β en/hi/fr/de/es; CHANGES category + hours wording.β ONE LANGUAGE PER DATASET.βββ batchSize β 1-20, default 5. A RATE-LIMIT dial. API/SDK/MCP ONLY.β Runs thinning out? Split the query; API callers can LOWER it.ββββ What is the actual question?β βββ contactable list β filter phone / website non-nullβ βββ best rated β PARSE rating to a number first (it is a string)β βββ busiest β PARSE reviewCount (string, may contain commas/brackets)β βββ mapping β latitude/longitude, fall back to plusCodeβ βββ hours β split `hours` on " | "β βββ unique list β dedupe on url or plusCode (no dedup across queries)ββββ Should results land in another app?βββ yes β mcpConnector + mcpTool, then READ THE LOG's printed argument schemaβ CRM row each β perPlace (DEFAULT, uncapped)β digest β summary Β· long list β chunkedβββ no β deliveryMode "none"[Execute Actor]ββββΊ SUCCEEDED with rows β dedupe, parse the string numerics, then useββββΊ SUCCEEDED, 0 rows β READ THE LOG: Feed not found / No URLs collectedββββΊ 5 rows per query β free plan cap, not a prospect list
π° Cost Control & Pricing Transparency
Billing model: usage-based. This Actor bills for platform usage β the compute time and resources a run consumes β not per row. Current rates are on the Actor's Apify Store page; the Console shows the live total mid-run.
The shape to keep in mind: one feed scroll per query, then one browser page load per business.
Cost tracks the number of businesses, so maxResults Γ number of queries is your bill.
| Choice | Effect on cost |
|---|---|
maxResults | The main lever β it is the number of place pages opened, per query. |
| Number of queries | Multiplies it: four queries at 100 is up to 400 page loads. |
batchSize | Doesn't change the work, only how fast it's done β but too high causes retries and thinning. API-only. |
language | No cost effect. |
| Delivery mode | perPlace adds one connector call per business; summary adds one. |
Worked examples β relative run shape
| Scenario | Queries | Page loads | Rows | Notes |
|---|---|---|---|---|
Probe β 1 query, maxResults: 5 | 1 | ~6 | β€ 5 | Baseline. Confirms the query returns places. |
| Standard β 1 query, 50 places | 1 | ~51 | β€ 50 | The common prospecting run. |
| City coverage β 4 neighbourhoods Γ 100 | 4 | ~404 | β€ 400 | The right shape for real coverage. |
Over-ask β 1 query, maxResults: 500 | 1 | ~121 | ~120 | Wasted intent: Maps stops feeding; you get ~120. |
| Regional sweep β 20 queries Γ 100 | 20 | ~2,020 | β€ 2,000 | Set maxTotalChargeUsd. |
Keeping runs predictable:
- First run: one query,
maxResults: 5. Confirms the query wording returns the right places. - Split geography rather than raising
maxResultsβ past ~120 the higher number buys nothing. batchSizestays at 5 unless you call the API β the field is hidden in the Console form. Lower it from an API/SDK/MCP call if results thin out.- Always set
maxTotalChargeUsdon multi-query sweeps. - Filter for contactable rows after the fact β you can't ask Maps for "only listings with a phone", so budget for the ones without.
- Free Apify plans get 5 places per query β a connectivity check, not a prospect list.
π οΈ Troubleshooting
Most problems are query craft, the two string-typed numeric fields, or asking for more than Maps will feed β rarely a scraper fault.
| # | Symptom you see | Most likely cause | Fix |
|---|---|---|---|
| 1 | Empty dataset | No queries supplied, or the feed never loaded | Check the log for π Query: lines and Feed not found for β¦. |
| 2 | Log: Feed not found for a query | Maps did not render a results feed for that search | Simplify the wording and retry. If it happens across queries, lower batchSize from an API/SDK/MCP call. |
| 3 | Log: No URLs collected for '<query>' | The search genuinely has no places | Broaden or rephrase β check the same query on maps.google.com. |
| 4 | Asked for 500, got ~120 | Google stops feeding a single search at roughly 120 places | Split into neighbourhood or category-narrowed queries. |
| 5 | Rating sort is nonsense | rating is a string | Parse to a number before sorting or averaging. |
| 6 | reviewCount won't parse | It's display text β "1,234", "(1,234)" or "1,234 reviews" | Strip non-digits, then parse. |
| 7 | Consumer throws on tags | It's null, not [], when there are no chips | Guard for null before iterating. |
| 8 | Categories in two languages | Different language values across runs | One language per dataset. |
| 9 | Half the rows have no phone | Many listings publish none | Expected β filter for contactable rows. |
| 10 | latitude/longitude null | The resolved URL carried no coordinates | Fall back to plusCode or geocode address yourself. |
| 11 | No place_id anywhere | It isn't exposed | Use url or plusCode as your key. |
| 12 | The same business appears twice | It matched two queries; each row is tagged | Dedupe on url or plusCode. |
| 13 | Run is slow, or results thin out mid-run | batchSize too high β there is no proxy input to tune | Split the query into smaller searches. From an API/SDK/MCP call you can also drop batchSize to 2β3. |
| 14 | β οΈ No data extracted on some places | Those listings didn't render a detail panel in time | Partial by design; re-run for the gaps if they matter. |
| 15 | Only 5 places per query | Free plan cap | Upgrade; the log says Free plan detected β capping results to 5 per query. |
| 16 | Hundreds of Slack messages | deliveryMode defaults to perPlace | Use summary or chunked for channels; keep perPlace for CRMs. |
| 17 | Every connector call rejected | mcpArguments doesn't match the tool's schema | Read Connector [tool] expects arguments matching this schema: in the log and copy that shape. |
| 18 | Airtable / Notion received nothing | Delivery needs both mcpConnector and mcpTool | Set both. Every dataset row is still saved. |
| 19 | HTTP 401 / 403 from the Apify API | Missing, expired or malformed APIFY_TOKEN | Regenerate in Console β Settings β API & Integrations. |
| 20 | HTTP 429 / 408 from the Apify API | Apify rate limits / the sync endpoint's time ceiling | Back off (2s β 4s β 8s), or switch to async POST /runs + polling. |
Diagnostic checklist before opening an issue
- Run the same query on maps.google.com β if it returns little there, the Actor can't do better.
- Read the
β³ URLs found:lines β they tell you whether the problem is discovery or extraction. - Baseline run: one query,
maxResults: 5,deliveryMode: "none". - Check you're not fighting the ~120 ceiling before assuming results are missing.
- Confirm you parsed
rating/reviewCountbefore trusting any ranking. - Lower
batchSizeto 2 from an API/SDK/MCP call and re-run if listings are being skipped in bulk.
If the issue survives all six, open an Issues ticket (or email support) with the run ID, the input you used, and what you expected.
β FAQ
Do I need a Google account or a Places API key?
No. The Actor reads publicly visible Maps listings. You need an Apify account and API token; you need no Google credentials, API key or Cloud billing project.
Is this the official Google Places API?
No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Google LLC. The Places API is the sanctioned route and worth using if its billing and caching terms suit you.
Can I plug it into Claude, Cursor, or a LangChain agent?
Yes β it is a native MCP tool: claude mcp add --transport http apify "https://mcp.apify.com?tools=techforce.global/google-maps-scraper"
How do I get more than about 120 results for a city?
You split the city. Google Maps stops feeding any single search at roughly 120 places no matter what maxResults says, so four neighbourhood queries return far more than one city-wide query. Dedupe on url afterwards, because the boundaries overlap.
Is maxResults per query or per run?
Per query. Three queries at 100 is up to 300 rows in one run.
Why does my rating sort come out wrong?
Because rating and reviewCount are captured as displayed text, not numbers β reviewCount can even arrive as "(1,234)" or "1,234 reviews". Strip the non-numeric characters and parse both before sorting or averaging.
Why is tags sometimes null instead of an empty array?
Because the extractor returns null when a listing has no attribute chips at all. Guard for null before iterating, or coalesce to [] at ingest.
What does the language setting actually change?
More than the interface. Category names, opening-hours labels and tag wording all come back in the chosen language, so category values differ between an en run and a de run. Keep one language per dataset or grouping breaks.
Can I use a proxy?
Thereβs no proxy input on this Actor β it runs from Apifyβs own IPs. If a large run slows down or results thin out, split the search into smaller queries. API, SDK and MCP callers can also lower batchSize to 2β3; the field is hidden in the Console form.
What is batchSize for?
Itβs how many place pages are opened at once. Default 5, range 1β20. Higher is faster but uses more memory and is more likely to trip Googleβs rate limiting, so treat it as a throttle rather than a speed setting. It is hidden in the Console form and settable only via API, SDK or MCP.
Why do so many rows have no phone or website?
Because many businesses simply don't publish them on their listing. Both fields are null in that case β filter for contactable rows rather than assuming every result is reachable.
Is there a place_id?
No. Google's place_id isn't exposed. Use url as your join and de-duplication key, or plusCode where you need a location-stable identifier.
Why does the same business appear twice?
Because it matched two of your queries, and each row is tagged with its own searchQuery so per-query analysis stays honest. De-duplicate on url or plusCode for a unique list.
Can I get review text, popular times or menus?
No. This Actor returns the listing's own facts β identity, contact, location, hours, rating and review count. Review text, reviewers, popular times, menus and booking links are all out of scope.
Does it contact the businesses for me?
No, and it can't. MCP delivery pushes rows into connectors you authorised β Airtable, Notion, Slack, Sheets, HubSpot. Nothing is ever sent to a business.
Which delivery mode should I use?
perPlace (the default) when you want one CRM record per business β that's exactly what it's for. summary or chunked for a digest into a channel, since perPlace would post one message per business. none for dataset-only runs.
My connector calls all fail. What now?
Your mcpArguments shape doesn't match what the tool expects. Run once with the connector and tool set, then read the log line Connector [tool] expects arguments matching this schema: β it prints the connector's own JSON Schema. The dataset saves regardless, so a failed delivery costs you nothing but the retry.
Is the contact data personal data under GDPR?
Sometimes, yes. A switchboard number for a large firm generally isn't, but a sole trader's mobile and home address on their listing very much can be. Read Compliance & Data Privacy before you load it into an outreach tool.
What does it cost, and what do I get on the free plan?
Usage-based: you pay for the platform usage a run consumes, not per row. It's browser-driven β one page load per business β so the total number of businesses is the cost driver. Free Apify plans return 5 places per query. Pass maxTotalChargeUsd for a hard per-run ceiling.
π Companion machine-readable files
| File | What it defines |
|---|---|
| .actor/input_schema.json | Every input, its type, default and bounds, plus the language and delivery enums |
| .actor/dataset_schema.json | The three Console views β Overview, Contacts only, Map & media |
| .actor/output_schema.json | Where the run's output is exposed |
| .actor/actor.json | Actor identity and storage wiring |
If this page and the schema files disagree, the schema files win. They are what the platform validates against at run time; this page is documentation and can lag a release.
Note that
dataset_schema.jsondefines views but no per-field JSON Schema, so the platform does not type-check pushed rows. The field table above is documentation of what the extractor produces, not a contract the platform enforces.
π Related Actors by Techforce Global
This is the general-purpose Maps scraper. The five siblings below are the same data source pre-aimed at one sector β worth checking before you build sector logic yourself.
| Actor | Best for |
|---|---|
| This Actor | Any category, any query β full control over the search |
| Google Maps Leads β Sales Intelligence Tool | Sales-intelligence enrichment layered on Maps leads |
| Finance Google Maps Lead Scraper | Finance and professional-services firms |
| Construction Lead Intelligence Scraper | Construction, trades and contractors |
| Google Maps Healthcare Leads β Sales Intelligence Tool | Healthcare providers, clinics and practices |
| Google Maps Hospitality Lead Scraper | Hotels, restaurants and hospitality venues |
Pick by intent: if your prospecting is confined to one of those sectors, start with the vertical Actor β check its page for the exact fields it adds on top of the listing data. If you need categories they don't cover, or you want to write the query yourself, use this one.
Pipeline patterns: territory build β one query per neighbourhood, dedupe on url, load
into Airtable via perPlace Β· competitive density β the same category across many areas, then
count rows and compare parsed ratings per area Β· map layer β latitude/longitude straight
into your GIS or mapping tool Β· refresh cadence β the same queries on a schedule, joined on
url, to catch new openings and closures.
Browse all Actors by Techforce Global at scraper.techforce.global.
π Compliance & Data Privacy
Trademark & affiliation. This is an independent Actor. It is not affiliated with, endorsed by, or sponsored by Google LLC. Google Mapsβ’ is a trademark of Google LLC.
-
Public business listings only. Everything returned is visible to any signed-out visitor. No account login, no reviewer identities, no review text, no user photos, no private data.
-
β οΈ "Business data" is not automatically "not personal data." A switchboard number for a hospital chain is corporate information. A sole trader's mobile number and home address on their Maps listing is personal data under GDPR / UK GDPR β and the same applies to a named practitioner's direct line. This Actor collects both kinds and cannot tell them apart. Under GDPR the obligations that follow β lawful basis, transparency, a route to object β sit with you.
-
Outreach is a separate legal question from collection. Cold email and cold calling are governed by PECR/ePrivacy in the UK and EU, CAN-SPAM in the US, CASL in Canada and equivalents elsewhere, and several of those distinguish sole traders from incorporated businesses. A phone number appearing on a public listing is not consent to be contacted, and in many jurisdictions you must also screen against a do-not-call register. Get advice before running outreach at scale.
-
The Actor never contacts anyone. MCP delivery writes into connectors you authorised. What you then do with the rows is entirely your decision and your responsibility.
-
Minimise what you keep. If your question is "how many dentists are in this postcode and how are they rated?", you don't need
phoneat all. Drop the contact columns at ingest when the analysis doesn't need them β it's the cheapest compliance measure available. -
Ratings and counts are a snapshot. They move constantly, and
reviewCountis a text field that can be rendered differently by locale. Publishing "the average rating of X in Y" without the capture date, the sample size and a note on parsing produces a number that looks authoritative and isn't. -
Listing content belongs to Google and the business. Photos are hosted on Google's CDN and those URLs can rotate. Internal analysis, prospecting and mapping sit on far safer ground than republishing Maps listings as your own directory.
-
Google's Terms of Service prohibit automated access and scraping, and the sanctioned route is the Places API. This Actor does not remove that risk β it is a risk you accept by running it. Keep volumes proportionate to the question you're answering.
-
Attribution over mirroring. Every row carries
url. Link back to the listing rather than presenting Google's local index as your own.
π Support & Custom Pipeline Engineering
Need automated multi-city scheduled feeds, webhook enrichment, or enterprise data warehouse syncs?
- Email: bhavin.shah@techforceglobal.com
- Custom Enterprise Integrations: Book a 15-Minute Technical Consultation
- Maintained by: Techforce Global β Specialists in High-Performance Web Scrapers and Agentic Workflows.
π·οΈ Structured data for search & AI discovery
SoftwareApplication describes the Actor; FAQPage wraps the FAQ above so those answers are
eligible for rich results, matching it question-for-question.
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Google Maps Scraper","alternateName": "Google Maps Scraper β Business Listings, Contacts & Coordinates","applicationCategory": "BusinessApplication","applicationSubCategory": "Web Scraping / Local Business Data & Lead Generation","operatingSystem": "Cloud (Apify platform)","description": "Runs any Google Maps search β one query or many β and returns one row per business: name, category, rating, review count, address, phone, website, opening hours, attribute tags, latitude and longitude, Plus Code, listing photo, the Maps URL and the query that produced the row. No Google account, Places API key or billing project is required. Five interface languages, configurable page concurrency, and optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β one call per business for CRM-ready records.","url": "https://apify.com/techforce.global/google-maps-scraper","author": {"@type": "Organization","name": "Techforce Global","url": "https://www.techforceglobal.com"},"provider": {"@type": "Organization","name": "Apify","url": "https://apify.com"},"offers": {"@type": "Offer","priceCurrency": "USD","category": "Pay per platform usage","description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Browser-driven, so cost tracks the number of businesses scraped. Current rates are shown on the Actor's Apify Store page.","url": "https://apify.com/techforce.global/google-maps-scraper"},"featureList": ["No Google account, Places API key or billing project required","Any free-text Google Maps search, one query or many in a single run","Every row stamped with the search query that produced it","Name, category, rating and review count from the listing","Phone, website and full address for contactable prospects","Opening hours captured as a weekly string","Latitude, longitude and Plus Code for mapping and territory work","Attribute tags and the listing photo","Five interface languages: English, Hindi, French, German, Spanish","Configurable page concurrency via API, SDK or MCP to balance speed against rate limiting","Rows saved as each listing is parsed, so aborted runs keep partial data","Per-listing failures isolated - one bad page never sinks the run","Optional MCP delivery with one connector call per business for CRM records","Connector argument schema printed to the log to make delivery configurable","Three Console dataset views including a CRM-ready Contacts view"],"isAccessibleForFree": false,"softwareRequirements": "Apify account and API token","keywords": "Google Maps scraper, Google Maps business scraper, Places API alternative, local business lead generation, extract phone numbers from Google Maps, business listings by city, GPS coordinates scraper, local SEO data, sales prospecting, MCP connector"}
{"@context": "https://schema.org","@type": "FAQPage","mainEntity": [{"@type": "Question","name": "Do I need a Google account or a Places API key?","acceptedAnswer": {"@type": "Answer","text": "No. The Actor reads publicly visible Maps listings. You need an Apify account and API token; you need no Google credentials, API key or Cloud billing project."}},{"@type": "Question","name": "Is this the official Google Places API?","acceptedAnswer": {"@type": "Answer","text": "No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Google LLC. The Places API is the sanctioned route and worth using if its billing and caching terms suit you."}},{"@type": "Question","name": "Can I plug it into Claude, Cursor, or a LangChain agent?","acceptedAnswer": {"@type": "Answer","text": "Yes β it is a native MCP tool: claude mcp add --transport http apify \"https://mcp.apify.com?tools=techforce.global/google-maps-scraper\""}},{"@type": "Question","name": "How do I get more than about 120 results for a city?","acceptedAnswer": {"@type": "Answer","text": "You split the city. Google Maps stops feeding any single search at roughly 120 places no matter what maxResults says, so four neighbourhood queries return far more than one city-wide query. Dedupe on url afterwards, because the boundaries overlap."}},{"@type": "Question","name": "Is maxResults per query or per run?","acceptedAnswer": {"@type": "Answer","text": "Per query. Three queries at 100 is up to 300 rows in one run."}},{"@type": "Question","name": "Why does my rating sort come out wrong?","acceptedAnswer": {"@type": "Answer","text": "Because rating and reviewCount are captured as displayed text, not numbers β reviewCount can even arrive as \"(1,234)\" or \"1,234 reviews\". Strip the non-numeric characters and parse both before sorting or averaging."}},{"@type": "Question","name": "Why is tags sometimes null instead of an empty array?","acceptedAnswer": {"@type": "Answer","text": "Because the extractor returns null when a listing has no attribute chips at all. Guard for null before iterating, or coalesce to [] at ingest."}},{"@type": "Question","name": "What does the language setting actually change?","acceptedAnswer": {"@type": "Answer","text": "More than the interface. Category names, opening-hours labels and tag wording all come back in the chosen language, so category values differ between an en run and a de run. Keep one language per dataset or grouping breaks."}},{"@type": "Question","name": "Can I use a proxy?","acceptedAnswer": {"@type": "Answer","text": "Thereβs no proxy input on this Actor β it runs from Apifyβs own IPs. If a large run slows down or results thin out, split the search into smaller queries. API, SDK and MCP callers can also lower batchSize to 2β3; the field is hidden in the Console form."}},{"@type": "Question","name": "What is batchSize for?","acceptedAnswer": {"@type": "Answer","text": "Itβs how many place pages are opened at once. Default 5, range 1β20. Higher is faster but uses more memory and is more likely to trip Googleβs rate limiting, so treat it as a throttle rather than a speed setting. It is hidden in the Console form and settable only via API, SDK or MCP."}},{"@type": "Question","name": "Why do so many rows have no phone or website?","acceptedAnswer": {"@type": "Answer","text": "Because many businesses simply don't publish them on their listing. Both fields are null in that case β filter for contactable rows rather than assuming every result is reachable."}},{"@type": "Question","name": "Is there a place_id?","acceptedAnswer": {"@type": "Answer","text": "No. Google's place_id isn't exposed. Use url as your join and de-duplication key, or plusCode where you need a location-stable identifier."}},{"@type": "Question","name": "Why does the same business appear twice?","acceptedAnswer": {"@type": "Answer","text": "Because it matched two of your queries, and each row is tagged with its own searchQuery so per-query analysis stays honest. De-duplicate on url or plusCode for a unique list."}},{"@type": "Question","name": "Can I get review text, popular times or menus?","acceptedAnswer": {"@type": "Answer","text": "No. This Actor returns the listing's own facts β identity, contact, location, hours, rating and review count. Review text, reviewers, popular times, menus and booking links are all out of scope."}},{"@type": "Question","name": "Does it contact the businesses for me?","acceptedAnswer": {"@type": "Answer","text": "No, and it can't. MCP delivery pushes rows into connectors you authorised β Airtable, Notion, Slack, Sheets, HubSpot. Nothing is ever sent to a business."}},{"@type": "Question","name": "Which delivery mode should I use?","acceptedAnswer": {"@type": "Answer","text": "perPlace (the default) when you want one CRM record per business β that's exactly what it's for. summary or chunked for a digest into a channel, since perPlace would post one message per business. none for dataset-only runs."}},{"@type": "Question","name": "My connector calls all fail. What now?","acceptedAnswer": {"@type": "Answer","text": "Your mcpArguments shape doesn't match what the tool expects. Run once with the connector and tool set, then read the log line \"Connector [tool] expects arguments matching this schema:\" β it prints the connector's own JSON Schema. The dataset saves regardless, so a failed delivery costs you nothing but the retry."}},{"@type": "Question","name": "Is the contact data personal data under GDPR?","acceptedAnswer": {"@type": "Answer","text": "Sometimes, yes. A switchboard number for a large firm generally isn't, but a sole trader's mobile and home address on their listing very much can be. Read the Compliance & Data Privacy section before you load it into an outreach tool."}},{"@type": "Question","name": "What does it cost, and what do I get on the free plan?","acceptedAnswer": {"@type": "Answer","text": "Usage-based: you pay for the platform usage a run consumes, not per row. It's browser-driven β one page load per business β so the total number of businesses is the cost driver. Free Apify plans return 5 places per query. Pass maxTotalChargeUsd for a hard per-run ceiling."}}]}
