Category:Scraping Use CasesView as Markdown
Google Maps Scraper: How to Scrape Google Maps for Leads (2026)

Software Engineer
A Google Maps scraper turns a search like "coffee shop in Austin" into a lead list, with the name, phone, website, rating and address of every business Google shows. The fastest route is Scrape.do's Google Maps API, which returns up to 20 parsed places per call for 10 credits. If you'd rather own the pipeline, the Python lane further down scrapes Google's internal Maps endpoints directly.
Finding the best burger spot in the city you're visiting is EASY in Google Maps.
Extracting names, websites, and phone numbers of a few thousand businesses across tens of different cities?
A week's work.
Well, not anymore.
Google Maps is the largest local business database on the internet, and there are two sane ways to turn it into a spreadsheet:
| Lane 1: the Google Maps API | Lane 2: your own Python scraper | |
|---|---|---|
| What you write | About 20 lines, no parsing | Protobuf URLs and regex parsers |
| Lead fields per search call | Name, phone, website, rating, reviews, address, hours | Name, phone, website, rating, reviews, address, coordinates |
| Cost per call | 10 credits, up to 20 places | 10 credits per request (Google targets run on residential proxies by default) |
| Reviews | Dedicated endpoint | Google retired the old endpoint (details) |
| When Google changes something | Scrape.do updates the parser | You update the regex |
Updated in October 2026: added the one-call API lane with a real lead export, re-ran every Python script against live Google Maps, fixed the image and phone parsing, and replaced the reviews scraper after Google retired its endpoint.
Google Maps Scraper in One Call
The short lane runs through the Google Maps API, a Scrape.do plugin endpoint. A search query goes in and parsed JSON comes out, so proxies, CAPTCHAs and Google's protobuf responses never reach your code. Search, Place and Reviews each cost 10 credits per successful request.
Pull Business Leads into a CSV
The script pins a coffee shop search to downtown Austin with the ll parameter (@latitude,longitude,zoom) and walks two result pages, skipping any place_id it has already seen. The lead fields land in leads.csv:
import csv
import requests
TOKEN = "<your_token>"
URL = "https://api.scrape.do/plugin/google/maps/search"
params = {"token": TOKEN, "q": "coffee shop", "ll": "@30.2672,-97.7431,14z", "gl": "us", "hl": "en"}
PAGES = 2 # up to 6 pages (about 120 places) per query
leads, seen = [], set()
for start in range(0, PAGES * 20, 20):
params["start"] = start
for attempt in range(2): # retry a transient 502 once
resp = requests.get(URL, params=params, timeout=60)
if resp.status_code != 502:
break
if resp.status_code != 200:
break
for place in resp.json().get("local_results", []):
if place["place_id"] not in seen:
seen.add(place["place_id"])
leads.append(place)
fields = ["title", "phone", "website", "rating", "reviews", "address", "type"]
with open("leads.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
writer.writerows(leads)
print(f"{len(leads)} leads saved to leads.csv")
start moves in steps of 20, and Google Maps stops at about 120 results (6 pages) per query, so PAGES = 6 is the ceiling. Deep pages can come back as a transient 502 no results, which is why the loop retries once before giving up. For a sturdier retry policy with backoff, see Python requests retry.
What Came Back
Our October 2026 run took two calls and 20 credits, and it came back with 38 unique coffee shops (40 results, minus 2 that showed up on both pages). These are the first six rows of leads.csv, with the review count in brackets after each rating:
| Business | Phone | Website | Rating |
|---|---|---|---|
| Black Cat Coffee | (650) 640-6906 | blkcatcoffee.com | 5 (147) |
| Dear Diary Coffee | (512) 489-6757 | deardiary.coffee | 4.8 (677) |
| Mozart's Coffee Roasters | (512) 477-2900 | mozartscoffee.com | 4.5 (10,979) |
| Desnudo Coffee: East Austin | not listed | desnudocoffee.com | 4.8 (927) |
| Once Over Coffee Bar | not listed | onceovercoffeebar.com | 4.5 (694) |
| Comunidad Specialty Coffee & Cold Press Juice | (512) 504-0023 | not listed | 4.8 (127) |
Across all 38 rows, every place had a rating, 33 had a website, 23 had a phone, and 22 had both. A missing phone usually means the listing has none. We called the Place endpoint for two of the phoneless shops, and it had no phone for them either. Budget for gaps, and treat the website column as the second contact route.
What It Costs
A search page holds up to 20 places for 10 credits, so 1,000 places cost about 500 credits when the pages come back full. Place and Reviews calls cost the same 10 credits each.
The free plan includes 1,000 credits a month with no credit card. That is 100 Maps calls, or up to 2,000 listings, before you spend anything.
Get 1,000 free credits and run the script above
Going Past 120 Places per Query
To cover a whole city, run the same query from several ll points (one per neighborhood, zoom 14 or 15) and dedupe by place_id, the same way the script dedupes pages. Then swap the query for each category you sell to, such as dentist, roofing contractor or marketing agency.
For thousands of calls, the Async API with plugins queues the jobs for you. If you're still deciding what to do with the list, our post on lead generation and web scraping covers the use cases.
Enrich a Lead with Place Details
Each local_results[] entry carries the IDs for the other two endpoints. Pass place_id or data_cid to Place (it rejects data_id), and data_id or place_id to Reviews.
curl "https://api.scrape.do/plugin/google/maps/place?place_id=ChIJ5w-cPeS0RIYR-bmqjYRkpCY&token=<your_token>"
Place fills in what a search row leaves out: summary, rating_summary (reviews per star), plus_code, user_reviews, and, where Google has them, menu, popular_times and a full images array. Reviews get their own section below.
What Data Can You Scrape from Google Maps?
Google Maps serves public data in three layers, and a lead list only needs the first. Search listings carry name, category, address, phone, website, rating, review count, hours and coordinates. Place details add the description, popular times, menu link, plus code, rating breakdown and photos, and reviews bring stars, text, date and owner replies.
| Layer | Fields | API endpoint | Python script |
|---|---|---|---|
| Search listings | Name, types, address, phone, website, rating, review count, hours, coordinates | /plugin/google/maps/search |
searchScraper.py (no hours) |
| Place details | Description, hours, rating breakdown, plus code, menu, popular times, photos | /plugin/google/maps/place |
detailsScraper.py |
| Reviews | Stars, text, ISO date, translation, topics, owner replies | /plugin/google/maps/reviews |
reviewsApi.py (calls the API) |
Build Your Own Google Maps Scraper with Python
This lane talks to Google the way the Maps frontend does, through the same internal endpoints. It suits anyone who wants full control, or who is curious how Google actually ships Maps data. Two scripts cover search results and place details. Reviews are handled separately, since Google retired the endpoint the old reviews scraper used.
All requests go through Scrape.do to handle Google's bot protection. If you are working on other Google properties, the Google search results scraping guide covers the general SERP approach, and sibling guides cover Google News, Google Shopping and Google Scholar. The original scripts are in the GitHub repository.
Scraping Google Maps Search Results
Google Maps search results contain business names, ratings, review counts, categories, addresses, phone numbers, coordinates, and thumbnail images. The typical approach would be to render the page in a headless browser and parse the HTML, similar to scraping JavaScript-rendered pages. But Google exposes a better path.

The tbm=map search endpoint returns structured protobuf-over-JSON data instead of HTML. No BeautifulSoup, no CSS selectors, no waiting for JavaScript to render a page. The response is a serialized nested array that can be parsed with regex against predictable field patterns.
This approach is more stable than scraping rendered HTML because the data structure follows Google's internal schema rather than a front-end layout that changes with A/B tests.
And it is significantly faster: one HTTP request instead of a full browser page load.
Prerequisites
The search scraper uses only requests and standard library modules:
pip install requests
A Scrape.do account is needed for the API token. The free plan works for everything below; sign up at dashboard.scrape.do.

The token goes into the TOKEN variable at the top of each script. All scripts use the placeholder <your_token>.
Understanding the tbm=map Endpoint
Google Maps search queries can be sent to https://www.google.com/search?tbm=map instead of the standard Maps URL. The pb (Protocol Buffers) parameter controls which data fields the server returns. It acts as a field selector: !7i20 requests 20 results per page, and other segments enable metadata like coordinates, ratings, and place types.

The response starts with )]}'\n, an anti-XSSI prefix Google adds to prevent direct JSON hijacking. Strip this prefix and you get a massive nested JSON array.
Here is how the script sets up the search query and protobuf parameter:
import json
import re
import urllib.parse
import requests
TOKEN = "<your_token>"
SEARCH_QUERY = "museums in paris"
SEARCH_PB = (
"!1s{query}!7i20!10b1"
"!12m6!1m1!18b1!2m1!20e3!6m1!114b1"
"!17m1!3e1"
"!20m57!2m2!1i203!2i100!3m2!2i4!5b1"
"!6m6!1m2!1i86!2i86!1m2!1i408!2i240"
"!7m33!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3"
"!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2"
"!1m3!1e10!2b0!3e4!1m3!1e9!2b1!3e2!2b1!9b0"
"!15m8!1m7!1m2!1m1!1e2!2m2!1i195!2i195!3i20"
)
Each ! segment in the SEARCH_PB string is a protobuf field selector. The {query} placeholder gets URL-encoded before insertion, and the full target URL points to google.com/search?tbm=map. The same pb string still worked unchanged in our October 2026 re-run.
Sending the Request Through Scrape.do
Google Maps blocks direct requests from datacenter IPs and requires cookie/session handling to avoid CAPTCHAs. Rotating proxies are essential here. Scrape.do runs every google.* request on residential proxies by default, at 10 credits per request, so the super=true below only makes that explicit.
pb = SEARCH_PB.format(query=urllib.parse.quote_plus(SEARCH_QUERY))
TARGET_URL = (
f"https://www.google.com/search?tbm=map&hl=en&gl=us"
f"&q={urllib.parse.quote_plus(SEARCH_QUERY)}"
f"&pb={urllib.parse.quote(pb, safe='')}"
)
api_url = (
f"https://api.scrape.do/?token={TOKEN}"
f"&url={urllib.parse.quote(TARGET_URL)}"
f"&geoCode=us&super=true"
)
response = requests.get(api_url, timeout=60)
The target URL is URL-encoded and passed as the url parameter to api.scrape.do. The geoCode=us parameter ensures the proxy IP is US-based, which affects which business listings Google returns. The response validation checks for the )]}'\n prefix to confirm the response is genuine Google data, not an error page.
Parsing Place Data with Regex

The response is a deeply nested JSON array. Navigating it by array indices is brittle because Google shifts positions between updates. A regex-based approach targets predictable field patterns that remain consistent: rating and review count appear as rating, review_count], followed by the feature ID in hex format, the place name, a null separator, and a type array.
PLACE_PATTERN = re.compile(
r',\s*(\d\.\d),\s*(\d+)\]' # rating, review_count
r'.*?' # intervening metadata
r'"(0x[0-9a-f]+:0x[0-9a-f]+)"' # feature_id
r',\s*"([^"]{2,80})"' # name
r',\s*null' # null separator
r',\s*\[((?:"[^"]*"(?:,\s*)?)+)\]', # ["type1", "type2", ...]
re.DOTALL,
)
Five capture groups pull out the key fields: rating (e.g., 4.7), review count (e.g., 380507), feature ID (e.g., 0x47e671d877937b0f:0xb975fcfa192f84d4), place name (e.g., Louvre Museum), and type array (e.g., "Art museum", "Museum"). A seen set deduplicates by feature ID since the same place can match multiple times in the response.
For each regex match, the script grabs a context window (500 chars before, 5000 chars after) and searches for nearby fields:
for m in PLACE_PATTERN.finditer(text):
fid = m.group(3)
if fid in seen:
continue
seen.add(fid)
name = m.group(4)
start = max(0, m.start() - 500)
end = min(len(text), m.end() + 5000)
ctx = text[start:end]
addr_match = re.search(
r'"' + re.escape(name) + r',\s*([^"]+)"', ctx
)
address = addr_match.group(1).strip() if addr_match else None
Address extraction looks for the pattern "Place Name, address string" in the surrounding context. Website extraction grabs the first non-Google HTTPS URL. Coordinates come from the [null, null, latitude, longitude] pattern.
Image URLs needed a fix. In the October 2026 re-run, every photo URL sat on the grass-cs/ path, and the old gps-cs-s/ pattern matched none of the 20 places. The pattern now accepts both:
img_match = re.search(
r'"(https://lh3\.googleusercontent\.com/(?:gps-cs-s|grass-cs)/[^"]+)"', ctx
)
Getting Phone Numbers from the Search Response
The first version of this guide said phone numbers only come from the place detail endpoint. The October re-run proved that wrong. Every place in the tbm=map response carries its number in two formats, [["+33 1 44 50 43 00",[["01 44 50 43 00"..., somewhere between the first and last mention of its feature ID.
spots = [x.start() for x in re.finditer(re.escape(fid), text)]
phone_match = re.search(r'\[\["(\+[\d\s().-]+)",\[\["', text[spots[0]:spots[-1]])
phone = phone_match.group(1) if phone_match else None
Searching only inside that span keeps a phone from leaking into the next place's record. In the October 2026 run, it returned 20 phones for 20 places, all different, and the Orangerie's matched the number the detail endpoint returns.
That changes the lead math for the DIY lane. One search request (10 credits) now returns 20 businesses with phone and website, and none of them needs a detail call.
Saving Search Results to JSON
The results write to maps_search_results.json with ensure_ascii=False for international characters (French museum names, accented addresses).
with open("maps_search_results.json", "w", encoding="utf-8") as f:
json.dump(all_results, f, indent=2, ensure_ascii=False)

The terminal output above is from the March 2026 run. The October run adds the phone field.
The October 2026 run for "museums in paris" returned 20 places, each with a phone, website, address, coordinates and image. The Louvre shows rating 4.7, 380,507 reviews, types ["Art museum", "Museum", "Tourist attraction"], address "75001 Paris, France", phone +33 1 40 20 53 17, and coordinates 48.86/2.34.
The feature ID is the key field here. It is the unique identifier that connects search results to the detail scraper and the reviews endpoint.
Extracting Place Details from a Single Location

Search results already carry the lead basics. A full place detail page adds the editorial description, business hours for every day, and higher-resolution images.
Google exposes place details through a /maps/preview/place endpoint that also returns protobuf-over-JSON. The input is a feature ID, which can come from the search results scraper or directly from a Google Maps URL.
Look at this URL for Musee de l'Orangerie:
https://www.google.com/maps/place/Mus%C3%A9e+de+l'Orangerie/...
/data=!4m10!...!1s0x47e66e2eeaaaaaa3:0xdc3fd08aa701960a!...
The feature ID 0x47e66e2eeaaaaaa3:0xdc3fd08aa701960a is right there in the URL. That is the input the detail scraper needs.
Building the Detail Request
The /maps/preview/place endpoint takes a pb parameter with the feature ID embedded. The pb string includes field selectors for all detail data.
TOKEN = "<your_token>"
FEATURE_ID = "0x47e66e2eeaaaaaa3:0xdc3fd08aa701960a"
DETAIL_PB = (
"!1m14!1s{fid}"
"!3m9!1m3!1d5000!2d0!3d0!2m0!3m2!1i1024!2i768!4f13.1"
"!4m2!3d0!4d0"
"!13m1!2m0"
"!15m47!1m8!4e2!18m5!3b0!6b0!14b1!17b1!20b1!20e2!4b1"
"!10m1!8e3!11m1!3e1!17b1!20m2!1e3!1e6!24b1!25b1!26b1!29b1"
"!30m1!2b1!36b1!43b1!52b1!55b1!56m1!1b1"
"!65m5!3m4!1m3!1m2!1i224!2i298"
"!22m1!1e81!29m0!30m6!3b1!6m1!2b1!7m1!2b1!9b1!32b1!37i771"
)
The !1s{fid} segment injects the feature ID. Other segments request specific data fields: !1i1024!2i768 sets the viewport for photo resolution, !4e2 enables review metadata, !17b1 enables business hours. The target URL points to google.com/maps/preview/place with the encoded pb parameter.
The Scrape.do API call uses the same super=true and geoCode=us parameters as the search scraper:
TARGET_URL = (
f"https://www.google.com/maps/preview/place"
f"?authuser=0&hl=en&gl=us"
f"&pb={urllib.parse.quote(pb, safe='')}"
)
api_url = (
f"https://api.scrape.do/?token={TOKEN}"
f"&url={urllib.parse.quote(TARGET_URL)}"
f"&geoCode=us&super=true"
)
response = requests.get(api_url, timeout=60)
The response validation checks for the same )]}'\n anti-XSSI prefix. If it is missing, the response is likely an error page or CAPTCHA.
Extracting Detail Fields
Each field is extracted with a targeted regex pattern against the serialized response. Same strategy as the search scraper, but with more field-specific patterns.
Name and types:
fid_match = re.search(
r'"(0x[0-9a-f]+:0x[0-9a-f]+)",'
r'\s*"([^"]+)",\s*null,'
r'\s*\[((?:"[^"]*"(?:,\s*)?)+)\]',
text,
)
name = fid_match.group(2) if fid_match else None
types = re.findall(r'"([^"]+)"', fid_match.group(3)) if fid_match else []
The regex matches "feature_id", "name", null, ["type1", "type2"]. Rating and review count sit before the feature ID block in the response, following the rating, review_count] pattern.
The full address appears as "Place Name, full address string" in the response. The detail page returns the complete address including street number, which the search results sometimes truncate.
Website extraction is more specific here. The URL appears before the feature ID, paired with a clean domain string:
if fid_match:
before_fid = text[:fid_match.start()]
for wm in re.finditer(
r'"(https?://[^"]+)",\s*"([a-z0-9][a-z0-9.-]+\.[a-z]{2,})"',
before_fid
):
url = wm.group(1)
if "google" not in url and "gstatic" not in url:
website = url
break
The phone pattern got wider in the October update. The old one, "(\+\d[\d ]+\d)", only allowed digits and spaces. That works for Paris (+33 1 44 50 43 00), but it skips US numbers like +1 512-477-2900 and falls back to a raw +15124772900 further down the response. This version also accepts hyphens, dots and parentheses:
phone_match = re.search(r'"(\+\d[\d\s().-]{6,}\d)"', text)
phone = phone_match.group(1) if phone_match else None
Coordinates use the same [null, null, lat, lng] pattern. Editorial description sits in arrays like [null, "Description text", null, null, null, 1], and the regex filters out URLs and hex strings to avoid false matches. The raw text keeps JSON escapes, so the script decodes the match with json.loads(f'"{candidate}"'); otherwise "live music & patio" comes out as live music \u0026 patio.
Business hours require their own pattern. Each day appears as ["Monday", ...[[" 9 AM-5 PM":
hours = {}
days_pattern = re.findall(
r'\["(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)"'
r'.*?\[\["([^"]+)"',
text,
)
for day, time_str in days_pattern:
if day not in hours:
cleaned = re.sub(r"[\u200b\u202f\xa0]", " ", time_str)
hours[day] = cleaned.replace("\u2013", "-").replace("\u2014", "-")
Unicode whitespace characters (\u200b, \u202f, \xa0) need cleaning, and Google's special dash characters get normalized to standard hyphens. (Ask me how I found out about the invisible zero-width spaces. The hard way.)
Saving Place Details to JSON
The script assembles all 13 fields into a single dict and writes to maps_place_details.json.

The October 2026 output for Musee de l'Orangerie shows: rating 4.6, 25,142 reviews, types ["Art museum", "Museum"], full address "Jardin des Tuileries, 75001 Paris, France", phone "+33 1 44 50 43 00", website, coordinates, the one-line editorial description (it mentions the 8 Monet water lilies murals), and business hours for all 7 days (Tuesday: Closed, Friday: 9 AM-9 PM).
We also pointed the script at a US business from the API run, Mozart's Coffee Roasters in Austin. That check is what made us widen the phone pattern. With the wider one, it returned the formatted phone +1 512-477-2900, the website, hours for all 7 days and the description.
How Do You Scrape Google Maps Reviews in 2026?
Google retired the review endpoint most DIY scrapers relied on, and its replacement rejected every stateless request we sent. For now, that leaves a real browser session per place or a reviews API that handles the session for you. Scrape.do's Reviews endpoint returns stars, text, dates and translations for 10 credits per call.

Reviews hold rating, text, timestamp, reviewer badge (e.g., "Local Guide"), visit details and language. For businesses analyzing customer sentiment or researching competitors, they are the most actionable data layer on Google Maps.
What Broke
The first version of this guide called /maps/preview/review/listentitiesreviews with the feature ID converted into two signed 64-bit integers, then paged through reviews 10 at a time. It worked in March 2026. In October 2026, the same request returned Google's HTML 404 page.
The Maps frontend now loads reviews from /maps/rpc/listugcposts. We tried it through Scrape.do. Without a session block in the pb string it returned a 400, and with one the request failed on every attempt. It appears to need the state of a real Maps tab, which means running a browser per place.
Reviews Through the API
Until that settles, the reliable path is the Reviews endpoint. It takes the same feature ID the search scraper returns (data_id in API terms):
import json
import requests
TOKEN = "<your_token>"
params = {
"token": TOKEN,
"data_id": "0x47e66e00f9521b7d:0xc8c16b75253918c1", # Carnavalet Museum
"sort_by": "newest",
}
resp = requests.get("https://api.scrape.do/plugin/google/maps/reviews", params=params, timeout=60)
data = resp.json()
info = data["place_info"]
print(f"{info['title']}: {info['rating']} stars from {info['reviews']} reviews")
for topic in data.get("topics", [])[:5]:
print(f" topic: {topic['keyword']} ({topic['mentions']} mentions)")
for review in data["reviews"]:
text = review.get("snippet", "(rating only, no text)") # some reviews have no text
print(f"{review['rating']} stars, {review['iso_date'][:10]}: {text[:70]}")
with open("maps_reviews.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
In our October 2026 run, the call returned Carnavalet Museum's header (4.7 stars from 12,976 reviews), 10 review topics and the 5 newest reviews. The biggest topic was "free admission" with 712 mentions, and the latest review had been posted 11 minutes before the call. French reviews came back with the original text and an English translation side by side in extracted_snippet. Three of the five were rating-only, with no snippet key at all, which is why the script reads it with .get().
sort_by takes newest, highest_rating or lowest_rating, and leaving it out gives Google's relevance order. The older spelling newestFirst came back as a 400 in our test, with a validation message listing the values above. To narrow the list, pass a topic id from the first response back as topic_id, or filter by keyword with query. The API takes one or the other, not both.
FAQ
How many results does a Google Maps scraper get per search?
Both lanes get up to 20 places per request. The API's local_results[] holds up to 20, and the !7i20 segment in the protobuf parameter asks for 20. Google Maps stops at about 120 results per query (6 pages), so bigger lists come from running more queries across more categories and ll points.
Is it legal to scrape Google Maps?
The data involved is public business-listing information, and plenty of tools collect it commercially. Reviews carry author names, so data-protection rules (GDPR and similar) apply to how you store and process them. Google's terms also restrict automated access. For a specific use case, especially one involving personal data, get proper legal advice rather than a blog paragraph.
Is the official Google Maps API free?
Only up to a point. Google's Places API includes a limited amount of free monthly usage, then bills per request and gates fields behind pricing tiers. Its terms also limit storing what it returns. All of that makes it a poor fit for building a lead database, and our Google Maps API comparison breaks down the official pricing and benchmarks five scraping APIs side by side.
Why use the tbm=map endpoint instead of scraping the regular Maps page?
The regular Google Maps page loads results through JavaScript rendering, requiring a headless browser. The tbm=map endpoint returns the same data as structured protobuf-over-JSON, parseable with regex. It is faster (one HTTP request instead of a browser page load), more reliable (no rendering failures), and cheaper (no render=true parameter required on Scrape.do).
What does the super=true parameter do?
super=true routes the request through Scrape.do's residential and mobile proxy pool. For google.* targets Scrape.do applies it by default, so every Google Maps request in the Python lane costs 10 credits whether you pass it or not. The plugin endpoints in the API lane also cost 10 credits and handle proxies on their own.
Can I filter Google Maps reviews by language?
The Reviews endpoint returns reviews in every language. With the default hl=en, non-English reviews include the original text and an English translation in extracted_snippet, so you can keep either one. Set hl to another language code to get translations and guided-review labels in that language instead.
If the goal is a lead list by Friday, start with the API lane, where one call returns up to 20 businesses with phone and website fields. The Python lane is for seeing exactly what Google sends and owning every regex. Both cost 10 credits per request, and the feature ID ties search results, place details and reviews together either way.
So what does this look like in practice? A single search query returns 20 businesses. Expand that across 50 cities and you have a lead list of 1,000 businesses with phone numbers, websites, ratings, and customer sentiment. That is the kind of dataset that turns cold outreach into informed outreach.

Software Engineer

