Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Learn how to scrape local business listings and Google Maps data: render map results, paginate, extract NAP fields, dedupe, and stay within Google's ToS.

Hari Sapna Nair
Author

Navin Chandra
Reviewer
Last Updated on: July 6, 2026
You need a batch of dentists across a few metro areas, each with a name, a street address, and a working phone number. The head-term tools that rank for "google maps scraper" hand you a black-box export and a monthly bill. This guide takes the engineering path instead: how the listing data is actually rendered, how to page through it, how to pull clean NAP fields, how to dedupe, and where the legal line sits.
The single most important fact up front: the compliant route for Google Maps and local business data is the official Places API. Google's Terms of Service prohibit "using automated means to access content from any of our services in violation of the machine-readable instructions on our web pages (for example, robots.txt files that disallow crawling)." Browser automation belongs on public sources you are permitted to scrape, and this guide covers that engineering while pointing you at the API where the API is the right answer.
Overview
What Does Scraping Local Business Listings Involve?
Collecting NAP data (name, address, phone) plus hours and ratings from map results and public directories, then deduping it into one clean row per business. The three NAP fields are the matching backbone, so getting them clean matters more than collecting noisy extras.
Which Route Should You Use?
What Is the One Boundary That Matters?
Google's Terms of Service prohibit automated access that bypasses machine-readable rules, so scraping Google Maps itself is off-limits. Browser automation belongs on public sources you are permitted to access, and undetectability is never something a tool can promise.
The short answer: scraping Google Maps itself sits against Google's Terms of Service, and the sanctioned path is the API. The nuance is worth understanding before you write a line of code, because "is a Google Maps scraper legal" is the question that decides your whole architecture.
This is the same discipline any responsible scraping project follows. Our guide on scraping dynamic web pages walks through the technical mechanics of JavaScript-rendered content that this article builds on for local listings.
For Google's own map data, the Places API is not just the compliant choice, it is usually the cheaper and more stable one at volume. For everything outside Google, browser automation is what gets you structured rows from pages that were never meant to be an API.
| Dimension | Places API (Google data) | Browser scraping (public sources) |
|---|---|---|
| Compliance | Sanctioned by Google, with published usage policies | Depends on the target site's terms; must be permitted |
| Cost model | Per-request billing; Place Details Pro is $17.00 per 1,000 | Browser infrastructure cost; no per-record license fee |
| Data freshness | Live query; place IDs storable indefinitely | As fresh as the page you render at scrape time |
| Coverage | Google's listing graph only | Any public directory or niche vertical site |
| Maintenance | Stable schema, versioned | Breaks when the site's DOM changes |
On Google Maps Platform pricing, Place Details Pro is $17.00 per 1,000 requests, and Text Search Pro and Nearby Search Pro are each $32.00 per 1,000 requests, with a monthly free usage allowance per SKU.
If Google's coverage is enough, the API is the compliant and stable choice. If you need niche vertical directories Google does not index cleanly, browser scraping is what gets you there, which is what the rest of this guide is about.
Local listing work revolves around NAP: name, address, and phone. These three fields are the deduplication and matching backbone of every business directory, so getting them clean matters more than collecting twenty noisy extras.
A note on framing: this is directory and map data collection, not a lead-generation pipeline. If your goal is pushing enriched contacts into a CRM, that is a different workflow with different consent obligations, and you should treat NAP collection and outreach as separate stages. Our guide to web scraping for lead generation covers that CRM-pipeline side.
Map and directory listings are single-page applications. A plain HTTP request returns an almost-empty HTML shell, and the listings are assembled in the browser by JavaScript after load. That is why a naive requests or curl call comes back with no data. You need a real browser that executes JavaScript and hydrates the page before you read the DOM.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://ecommerce-playground.lambdatest.io/")
# Wait for the listing container to hydrate, not a fixed sleep
page.wait_for_selector(".product-thumb", timeout=15000)
cards = page.query_selector_all(".product-thumb")
print(f"rendered {len(cards)} listing cards")
browser.close()The example above targets the LambdaTest Ecommerce Playground, a safe public sandbox with listing cards you can practice against without touching a third party's terms. For a deeper walkthrough of the same technique in Python, see the web scraping with Python guide.
Local results almost never fit on one page. Directory sites tend to use one of a few pagination patterns, and identifying which one you are facing is the difference between a scraper that collects the first screen of rows and one that collects the full result set.
# Infinite-scroll pattern: scroll until the row count stops growing
previous = 0
while True:
page.mouse.wheel(0, 4000)
page.wait_for_timeout(1500) # let lazy-loaded rows render
current = len(page.query_selector_all(".product-thumb"))
if current == previous:
break # no new rows loaded, we've reached the end
previous = current
print(f"collected {current} rows after pagination")The key discipline in every pattern is the same: wait on a real signal (row count changed, selector appeared), never a blind sleep. Blind sleeps make scrapers slow and flaky in equal measure.
Once the page is rendered, extraction is a matter of selecting each listing element and reading its fields. The trap is fragile selectors: absolute XPath and deep CSS chains break the moment the site tweaks its markup. Anchor to stable, meaningful attributes instead.
import re
def clean(text):
return re.sub(r"\s+", " ", (text or "").strip())
rows = []
for card in page.query_selector_all(".product-thumb"):
name_el = card.query_selector(".title a")
name = clean(name_el.inner_text()) if name_el else None
# In a real directory scrape, read address and phone the same way,
# scoped to this card element so fields never cross rows.
rows.append({"name": name, "source": page.url})
print(f"extracted {len(rows)} NAP rows")The same business shows up on multiple directory pages, in multiple categories, and with tiny formatting differences. Deduplication is what turns a messy multi-page dump into a clean, distinct dataset. Build a normalized key and collapse on it.
import re, hashlib
def dedupe_key(row):
name = re.sub(r"[^a-z0-9]", "", (row.get("name") or "").lower())
street = re.sub(r"\s+", " ", (row.get("street") or "").lower()).strip()
phone = re.sub(r"\D", "", row.get("phone") or "")
raw = f"{name}|{street}|{phone}"
return hashlib.md5(raw.encode()).hexdigest()
seen, unique = set(), []
for row in rows:
key = dedupe_key(row)
if key not in seen:
seen.add(key)
unique.append(row)
print(f"{len(rows)} rows collapsed to {len(unique)} unique listings")When you keep a record over a duplicate, prefer the most complete one. If two rows share a key but one has hours and the other does not, merge the non-null fields rather than dropping data. The same care around clean, stable extraction shows up in high-volume work like price scraping at scale.
A single-browser script works for a proof of concept. Collecting thousands of listings across regions means running many browsers in parallel, and maintaining that fleet yourself (patching Chrome, scaling servers, capturing failures) becomes the real cost. This is exactly the gap Browser Cloud fills: real, full-featured Chrome sessions provisioned on demand, so parallelism is a property of the platform rather than something you operate.
To validate this end to end, I ran a real Browser Cloud session against the LambdaTest Ecommerce Playground using the @testmuai/browser-cloud SDK. Here is the actual console output, rendering a JavaScript listing page and parsing candidate rows from the hydrated DOM:
[browser-cloud] starting real session against LambdaTest Ecommerce Playground
[browser-cloud] rendered page in 8.1s, extracted 2939 chars of text
[browser-cloud] sample product listings parsed from rendered DOM:
1. iMac
2. iMac
3. iMac
4. iMac
5. HTC Touch HD
6. HTC Touch HD
[browser-cloud] total candidate listing rows found: 6
[browser-cloud] doneAs the console output above shows, the session rendered the JavaScript-driven page and returned the hydrated text, from which the script parsed listing rows, the same shape of extraction you apply to a real directory. For a natural-language alternative that drives the same cloud browsers from the terminal or CI, the Kane CLI lets an agent open a page and extract values with a plain-English objective. You can read the full architecture in the Browser Cloud documentation. For building resilient scrapers with a modern automation framework, the Playwright for web scraping hub is the deeper companion to this guide.
Note: Hari Sapna Nair, Community Contributor at TestMu AI with expertise in Python and automation testing, reviewed, fact-checked, and approved this article, which was researched and drafted with AI assistance. Our editorial process and AI use policy describes how every claim is verified before publication. Run browser collection at scale on real Chrome with TestMu AI.
Start by deciding the source, because that decision is also the compliance decision. If you need Google's own map data, the Places API is the sanctioned path, and its usage policies tell you that place IDs are storable indefinitely while other content must be re-queried. For permitted public directories, a browser scraper that renders the SPA, paginates on real signals, extracts scoped NAP fields, and dedupes on a normalized key gets you a clean dataset.
The first concrete step: prototype against a safe sandbox like the LambdaTest playground, confirm your selectors and dedupe key hold, then scale the same script on TestMu AI Browser Cloud so the browser fleet is not yours to operate. Keep the target's terms of use in view at every stage, and treat stealth as a best-effort helper rather than a promise.
Author
Hari Sapna Nair is a Community Contributor with experience spanning software development, technical writing, and open-source collaboration. A former Technical Content Writer at TestMu AI, she has authored guides and tutorials on Selenium, testing, microservices, and emerging technologies. Sapna has contributed 60+ technical blogs for platforms like Coding Ninjas and has been an active open-source contributor in programs such as GirlScript Summer of Code and HakinCodes, securing top contributor ranks. A GHC Scholar and an engineer turned management student at IIM Indore.
Reviewer
Navin Chandra is a Member of Technical Staff at TestMu AI (formerly LambdaTest), building the open-source automation that powers its Selenium and Appium cloud grid. A committer to both Selenium and Appium, he implemented WebDriver BiDi support in Selenium for real-time browser events and bidirectional control and is developing Apple's iOS RemoteXPC protocol in Appium to enable low-level wireless communication with iOS system services. He contributes to Selenium across multiple language bindings as a member of the Selenium GitHub organization. He has served as a Google Summer of Code mentee and mentor at openSUSE and an LFX mentee at CNCF's KubeArmor, and is a SUSE Certified Deployment Specialist. Navin holds a B.Tech in Computer Science.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance