Hero Background

Next-Gen App & Browser Testing Cloud

Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Next-Gen App & Browser Testing Cloud
Web ScrapingBrowser AutomationGuide

How to Scrape Local Business Listings and Google Maps Data

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.

Author

Hari Sapna Nair

Author

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?

  • Google Maps data: use the official Places API. It is the sanctioned, stable route and returns the same NAP fields.
  • Other public directories: render each page in a real browser, since listings hydrate client-side. This is what TestMu AI Browser Cloud provides through its on-demand cloud browsers.

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.

Official API vs Browser Scraping: Which Do You Use?

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.

DimensionPlaces API (Google data)Browser scraping (public sources)
ComplianceSanctioned by Google, with published usage policiesDepends on the target site's terms; must be permitted
Cost modelPer-request billing; Place Details Pro is $17.00 per 1,000Browser infrastructure cost; no per-record license fee
Data freshnessLive query; place IDs storable indefinitelyAs fresh as the page you render at scrape time
CoverageGoogle's listing graph onlyAny public directory or niche vertical site
MaintenanceStable schema, versionedBreaks 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.

What Local Business Data Should You Collect?

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.

  • Name: The exact business name as displayed. Preserve casing for output but normalize a lowercase copy for matching.
  • Address: Full street address, ideally split into street, city, region, and postal code so you can dedupe on the street line alone.
  • Phone: Store the digits only as a normalized key, plus the display format for output. Phone is the single most reliable dedupe field.
  • Category and hours: Useful for filtering and enrichment, but treat them as optional and never let a missing category drop a row.
  • Source URL: Always keep the page you scraped it from. It is your audit trail when a record is disputed.

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.

How Do You Render Map Results?

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.

  • Launch a headless browser (Playwright or Puppeteer) and navigate to the listing page.
  • Wait for a stable selector that only appears after the listings hydrate, not for a fixed sleep.
  • Read the rendered DOM once the listing container is present.
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.

How Do You Paginate Through Listings?

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.

  • Numbered pages: Click or navigate to page 2, 3, 4 until the "next" control disappears. The most predictable pattern to automate.
  • Infinite scroll: Scroll to the bottom, wait for new rows to lazy-load, and repeat until the row count stops growing. Map result panels usually use this.
  • Load-more button: Click a "show more" button repeatedly and wait for the new batch to render each time.
# 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.

Test across 3000+ browser and OS environments with TestMu AI

How Do You Extract NAP Data Cleanly?

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.

  • Scope to the listing container first: Select the repeating card element, then read fields relative to each card so rows never bleed into each other.
  • Prefer semantic anchors: Class names tied to meaning (a phone link, an address block) survive redesigns better than positional selectors.
  • Normalize on read: Trim whitespace, collapse repeated spaces, and strip hidden Unicode such as non-breaking spaces so two addresses that look identical actually match later.
  • Capture the source URL per row: Store where each record came from at extraction time, not as an afterthought.
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")

How Do You Dedupe Local Listings?

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.

  • Lowercase the name and strip punctuation and legal suffixes (Inc, LLC).
  • Keep the street line of the address only, lowercased and space-collapsed.
  • Reduce the phone to digits only, dropping country and formatting characters.
  • Concatenate these into one key and hash it. Rows with the same hash are duplicates.
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.

Running a Google Maps Scraper 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.

  • Real Chrome rendering: JavaScript executes and the SPA hydrates, so the listings you read are the ones a user sees, not a stripped shell.
  • On-demand parallelism: Spin up many sessions for a burst of collection and release them when done, with no pre-purchased fleet to maintain.
  • Session transparency: Video, console logs, and network logs are captured automatically, so a run that returns zero rows is debuggable instead of a black box.
  • Best-effort stealth: Fingerprint masking, CAPTCHA handling, and ad blocking improve success rates on defended sites. They are best-effort, never a guarantee of undetectability, and you should still respect each target's terms.

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] done

As 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

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.

Conclusion

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

Blogs: 9

  • Twitter
  • Linkedin

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

Reviewer

  • Linkedin

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.

Open in ChatGPT Icon

Open in ChatGPT

Open in Claude Icon

Open in Claude

Open in Perplexity Icon

Open in Perplexity

Open in Grok Icon

Open in Grok

Open in Gemini AI Icon

Open in Gemini AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Google Maps Scraper FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests