Google Maps Scraper — Local Business Leads & GBP Gaps
Pricing
from $3.00 / 1,000 results
Google Maps Scraper — Local Business Leads & GBP Gaps
Scrape Google Maps businesses with name, address, phone, website, categories, rating, hours and coordinates, plus a missing-fields audit of each Business Profile. Filter to businesses with no website, no phone or an incomplete profile, so you get a qualified prospect list, not a place dump.
Pricing
from $3.00 / 1,000 results
Rating
0.0
(0)
Developer
mochi
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Scrape Google Maps business listings and get, for every business, which parts of its Google Business Profile are missing. Filter the run down to only the businesses with no website, no phone number or a neglected profile, and the dataset you pay for is a prospect list rather than a place dump you still have to qualify by hand.
Built for local SEO agencies, web-design and GBP-management sellers, franchise development teams and anyone doing territory research.
Pricing: Pay per result, at the rate shown in the pricing panel of this page, plus a very small per-run start fee. You are charged only for the results delivered.
What you get per business
| Field | Notes |
|---|---|
name, categoryPrimary, categories, categoryCount | Localised to your language setting (friseur in Leipzig with de returns Friseursalon) |
address, addressLines, neighborhood, cityRegion, countryCode | Null for the few businesses that publish no address, see limitations |
latitude, longitude, timezone | IANA timezone, e.g. America/Chicago |
phone, phoneE164 | phoneE164 is E.164: a +, the country code and the digits (+493412382180). It is built from the international form Google shows, so it is right outside the US too: valid on every row with a phone in our live runs for Germany (21 of 21), Singapore (19 of 19), the UK (12 of 12) and the US (20 of 20) |
website, websiteDomain | website is the business's own address; when Google sends its redirect wrapper instead, the real address is recovered (see limitations). websiteDomain is the registrable domain |
otherLinks | Booking pages, WhatsApp, Facebook: [{domain, url}] |
rating | Star rating, 1 to 5 |
reviewCount | Number of reviews behind the rating, as shown on the listing. An integer, or null for unrated businesses, never 0 |
openingHours | The whole week as Google lists it: [{day, hours}], seven entries in Google's order, starting with the business's current local day. hours is Google's own text: 9 AM–5 PM, 9 AM–1:30 PM, 2:30–6 PM (split shifts joined by , ), Closed, Open 24 hours. null when Google lists no hours |
hoursToday | {day, date, ranges} for the first day Google lists, which is the business's current local day |
openStatusText | e.g. "Open · Closes 3 PM", in your language |
photoUrl | The listing's profile picture: always a 44×44 pixel thumbnail on Google's image CDN, so a logo rather than a photo. About 28% of the links returned 404 in a 200-link check, see limitations |
listingLanguage | Language of the listing text |
missingFields, missingFieldCount | The audit: which of website / phone / hoursToday / logo / rating are absent |
placeId, featureId, googleMapsUrl | Stable identifiers for joining across runs |
searchTerm, searchRank, searchCountry, source, scrapedAt | Every row says where it came from |
How full those columns were in our test runs (396 distinct businesses from six unfiltered runs, 10 search terms, US and Germany, weighted towards trades that have profile gaps):
| Field | Non-empty |
|---|---|
name | 100.0% |
address | 98.7% |
phone | 92.4% |
rating | 97.2% |
reviewCount | 97.2% |
hoursToday | 89.4% |
openingHours | 89.4% |
photoUrl | 87.6% |
website | 74.5% |
| name + address + phone + rating, all four | 89.1% |
That sample is pooled and mostly US trades; other countries and trades look different. A Berlin café search had a phone number on 21 of 40 listings (53%), because Google lists none for the rest.
Filters (this is the point of the actor)
| Input | Effect | Checked in a live run |
|---|---|---|
onlyMissingWebsite | Keep only businesses with no website on the profile | 60 requested, 60 returned, 0 with a website |
onlyMissingPhone | Keep only businesses with no phone number | 10 requested, 10 returned, 0 with a phone |
minMissingFields | Keep only businesses missing at least N of the 5 audited elements | 2: 30 of 30 rows had 2 or more gaps |
maxRating / minRating | Rating ceiling / floor in whole stars only (1 to 5, so no 4.5), 0 disables | minRating: 5: 20 of 20 rated exactly 5 |
Filters are applied before the result cap, so maxResults: 60 with onlyMissingWebsite gives
you 60 website-less businesses, not 60 businesses of which a few happen to lack a website. The run
scans as many result pages as it needs: 6 pages for those 60 rows.
One asymmetry worth knowing: maxRating keeps businesses that have no rating at all — an
unrated profile is the strongest neglect signal in this data, so a ceiling should not exclude it.
minRating does drop them, because a floor is a quality bar. In a live maxRating: 4 run, 13 of
the 22 rows returned were unrated. That run returned 22 rows, not the 40 requested, because only 22
businesses passed the filter before the search term ran out (terminationReason: finished).
How to scale a run
One search term ends after a couple of hundred businesses. Three live runs of hair salon in Tulsa, OK
returned 196, 215 and 221 unique businesses (242, 260 and 241 results including repeats, 13 to 14
pages, 28 to 43 seconds) and then ran dry. Scale by adding terms and locations, not by raising
maxResults alone.
Two ways to say where:
// 1. the place is in the term{ "searchTerms": ["auto repair in El Paso, TX", "auto repair in Lubbock, TX"], "maxResults": 200 }// 2. a generic term swept over a coordinate grid{ "searchTerms": ["coffee"],"locations": [ {"latitude": 36.1539, "longitude": -95.9928, "radiusKm": 5},{"latitude": 25.7617, "longitude": -80.1918, "radiusKm": 5} ],"maxResults": 400 }
Every term × location pair is paginated independently and the result cap is shared evenly across
them, row by row: each pair gets the cap divided by the number of pairs, and when it does not divide
evenly the first pairs get one extra row. Seven terms at maxResults: 140 returned exactly 20 rows
each, and five terms at maxResults: 8 returned 2, 2, 2, 1 and 1 (live runs). A term that runs dry
early hands its share to the others rather than wasting it: with one term that found nothing and one
that found plenty, a cap of 30 returned 30 rows.
In a grid run searchRank restarts at 1 in every cell and rows carry no cell index; tell the cells
apart by latitude / longitude or cityRegion.
Google does not strictly bound results to the viewport: in three of four live runs of the coffee
example above, 2 of the 40 rows were in Brooklyn, NY. An invalid coordinate is skipped and counted
in the run summary, and the run continues with the valid ones; if every coordinate is invalid the
run fails with a message.
Speed in our runs of the final version: 20 rows in 2 to 4 seconds, 140 rows from seven terms in about 7 seconds, 589 rows from 30 terms in 34 seconds. Pages are fetched one after another and rows are written to the dataset in batches. A very large run stops itself before Apify's run timeout and says so, see the run summary below.
The finding that should shape how you use this
Incomplete profiles are concentrated deeper in the results, not on page one. Measured over all
196 unique businesses returned by one run of hair salon in Tulsa, OK (one trade in one city,
2026-10-17), by position in Google's result list:
| Result position | Businesses | No website | Avg. missing fields |
|---|---|---|---|
| 1–40 | 40 | 6 (15%) | 0.28 |
| 41–100 | 56 | 14 (25%) | 0.57 |
| 101–180 | 65 | 20 (31%) | 0.77 |
| 181–242 | 35 | 17 (49%) | 1.77 |
Two earlier runs of the same term (215 and 221 unique) showed the same shape: 0.30 average missing
fields in the first 40 results against 1.60 and 1.42 in the last band. All 10 unrated businesses in
the run above sat in the last band. For lead generation set maxResults to 100+. A 20-row run
shows you the incumbents.
Gap density also varies enormously by trade and city, and the counts move from run to run (
auto repair in El Paso, TX| Search term | No website in top 20 |
|---|---|
massage in Fresno, CA | 11 |
auto repair in El Paso, TX | 7 |
food truck in Memphis, TN | 7 |
barber shop in Lubbock, TX | 4 |
landscaping in Toledo, OH | 3 |
nail salon in Bakersfield, CA | 2 |
hair salon in Tulsa, OK | 1 |
dentist in Austin, TX | 0 |
Limitations, stated plainly
reviewCountis null for unrated businesses. Google sends no review count for a business with no rating, so it isnullfor those (11 of 396 sampled businesses, 2.8%) and never0. Where present it is the count Google shows next to the rating: a minimum of 1 and a median of 89 in the sample, and 70 of the 385 rated businesses had fewer than 25 reviews. The actor does not fetch individual reviews.- The week starts on the business's current local day.
openingHourslists seven days in Google's order, beginning with the day it is now at the business, in the business's own time zone: a Singapore business is already on Sunday while it is still Saturday in UTC (live run: 20 of 20 rows).hoursTodayis the first entry, with the date it describes, so an export is easy to place in time. - Very rarely the list starts on the previous day. Of 600 distinct businesses with hours in our
test runs, one (a pharmacy that is open nearly round the clock) was listed starting on yesterday,
so its
hoursTodayis that day's hours, labelled with that day's name and date. - Opening-hours text is Google's own, unmodified. Times have a narrow no-break space (U+202F)
before AM/PM and an en dash (U+2013) between times, so a search for
8 AMwith an ordinary space will not match; normalise the text if you parse it. It follows yourlanguagesetting:Geschlossenand24 Stunden geöffnetforde. - "No website" means no website field on the profile. Some of those businesses still have a
booking page or a Facebook link; those appear in
otherLinksso you can decide yourself. In a three live runs of the same seven searches (140 listings each), 25% to 30% had no website, and 3 to 5 of those listings per run still had another link. Google's own image-CDN and redirect URLs are stripped fromotherLinks, so an empty list means nothing else was published. - Google sometimes wraps the website in its own redirect. In some runs Google sends the website as
/url?q=<site>&opi=…&usg=…instead of the address: all 13 websites of one default run were wrapped and the next runs had none. The actor turns the wrapper back into the real address (checked on 33 real wrappers) andRUN_SUMMARY.websitesUnwrappedcounts how often that happened. A website value that cannot be turned into an address is left empty, listed as awebsitegap and counted inwebsitesUnusable; it is never the raw/url?…text. websiteDomainis the registrable domain Google reports, not always the full host: forhttps://goldensalontulsa.glossgenius.com/it isglossgenius.com. It equalled the host on 289 of 295 websites; the other 6 were subdomains on site-builder platforms.- Some businesses publish no address. 5 of 396 sampled businesses (1.3%), three landscapers
and two hair salons, have no address in their listing, so
address,cityRegionandcountryCodeare null for them. They still carry a phone and rating where Google shows one. - Single-category is not counted as a gap. About 60% of the listings in those runs (58% to 60%) had exactly one category, so counting it would mark most of a result set "incomplete" and make the score useless.
photoUrlis a logo thumbnail, and many links are dead. It is always the listing's 44×44 profile picture, so it counts as thelogogap (missingFieldssayslogo, notphoto), and a business can have a gallery of real photos that this actor does not return. In a seeded sample of 200 links, 56 (28%) returned HTTP 404, ranging from 0% to 36% by search term. Do not rely on the link being alive.- A very large run may stop early, and the summary says so. The actor stops itself shortly
before Apify's run timeout (a minute before it for the default one-hour timeout), keeps every row
it has and ends normally with
terminationReason: time_budgetandcomplete: false. In a live test with a 45-second run timeout, 30 search terms stopped by themselves at 34 seconds with 589 rows. - Not every business is reachable by search. This actor finds what Google's own search returns for your term; it is not an exhaustive registry of a city.
- Proxy. Requests go through Apify Proxy and each retry takes a fresh proxy session. The
default datacenter pool fetched all 279 result pages in our 59 completed test runs without a failure;
switch to
RESIDENTIALif you see failed pages at high volume.
Scope and privacy
- Public Google Maps search results only: no login, account or cookies are used.
- Business-level data only. Individual reviews and reviewer profiles are never requested (the only review data is the count Google shows on the listing), so no reviewer name can enter the dataset, and no output field is reviewer-, author- or owner-shaped.
- In our test runs no email address appeared anywhere in the output (0 across 396 businesses).
- This actor is not affiliated with or endorsed by Google.
Output sample
A real row from a live run (onlyMissingWebsite: true), trimmed to the interesting fields:
{"name": "LoneStar Automotive","categoryPrimary": "Auto repair shop","categories": ["Auto repair shop"],"address": "900 Hawkins A #1, El Paso, TX 79915","neighborhood": "Mission Valley","cityRegion": "El Paso, TX","countryCode": "US","latitude": 31.7642618,"longitude": -106.3802455,"timezone": "America/Denver","website": null,"websiteDomain": null,"phone": "(915) 234-7989","phoneE164": "+19152347989","otherLinks": [],"rating": 4.9,"reviewCount": 75,"hoursToday": { "day": "Saturday", "date": "2026-10-17", "ranges": ["8 AM–3 PM"] },"openingHours": [{ "day": "Saturday", "hours": "8 AM–3 PM" },{ "day": "Sunday", "hours": "Closed" },{ "day": "Monday", "hours": "Closed" },{ "day": "Tuesday", "hours": "8 AM–5 PM" },{ "day": "Wednesday", "hours": "8 AM–5 PM" },{ "day": "Thursday", "hours": "8 AM–5 PM" },{ "day": "Friday", "hours": "8 AM–5 PM" }],"openStatusText": "Open · Closes 3 PM","missingFields": ["website"],"missingFieldCount": 1,"placeId": "ChIJc6mrpidb54YROEs9oJbSS4k","googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJc6mrpidb54YROEs9oJbSS4k","searchTerm": "auto repair in El Paso, TX","searchRank": 11}
That row is the product in one screen: an auto shop with a 4.9 rating from 75 reviews, a phone number and a full week of opening hours, and no website.
Run summary
Every run that starts scraping writes a RUN_SUMMARY record to the key-value store, including a run
that stops early. Its main fields:
| Field | Meaning |
|---|---|
complete | true only when terminationReason is finished or max_results |
terminationReason | finished (every search ran out of results), max_results (your cap was reached), charge_limit (your maximum charge was reached), time_budget (stopped before the run timeout), blocked (no page could be fetched) or request_failed (some requests failed after retries, so the result is partial) |
rowsPushed | Rows written to the dataset |
It also counts pagesFetched, pagesFailed, filteredOut, duplicatesSkipped, shapeUnrecognised,
websitesUnwrapped, websitesUnusable and errors. A run that stops early or loses requests says so in complete and
terminationReason; one whose searches simply ran out of results is finished and complete.
Input that cannot be used fails the run at once, before any request and with no rows, and the
message says what to fix: no non-blank search term, a country or language that is not a 2-letter
code, or locations that are all invalid. An absent field means its default, so {} runs the demo
search.