← Back to blogTutorial

How to Scrape Google Maps for Local Business Leads

· 10 min read

If you are building a B2B lead list for any local-services niche — home services, professional services, healthcare, hospitality — Google Maps is the richest public data source in the world. A single search returns the business name, address, phone, website, category, full opening hours, geographic coordinates, and the review profile. The catch is that Google does not give you that data programmatically without major terms-of-service constraints, so most lead programs end up scraping the public Maps interface. This guide walks the full pipeline: building the search URL, running the scrape, cleaning the output, and chaining geographic slices to cover an entire city.

Why Google Maps Beats Other Lead Sources

The honest field comparison looks like this. ZoomInfo and Apollo bias toward enterprise titles and have weak coverage of local-services SMBs. Yellow Pages has good US coverage but rarely returns a website. The bought-list market is full of stale data with no provenance. Google Maps sits in the middle: every actively-operating business in any developed market is on it, the data is current because owners maintain it themselves, and every record includes the website needed to chain an email-enrichment step.

The other reason it wins is the geographic primitive. Every Maps query is anchored on a @lat,lng,zoom viewport, which means you can systematically grid-cover a city by varying the center point — something you cannot do with a category-and-city search on a directory site.

What Google Maps Actually Returns

Per business, a /search result gives you:

FieldExample
nameSmith Family Dental
address123 Yonge St, Toronto, ON M5C 1W4, Canada
address_parts["123 Yonge St", "Toronto", "ON M5C 1W4", "Canada"]
categoryDentist
categories["Dentist", "Cosmetic dentist", "Dental clinic"]
neighborhoodFinancial District
latitude43.6532
longitude-79.3832
rating4.7
reviews184
phone+1 416-555-0142
phone_raw4165550142
websitehttps://smithfamilydental.ca
website_labelsmithfamilydental.ca
timezoneAmerica/Toronto
hours{"Monday":"08:00–18:00", "Tuesday":"08:00–18:00", ...}
feature_id, cid, data_idGoogle's internal identifiers

What it does not include: email addresses, decision-maker names, employee count, or revenue. Those require a separate enrichment step keyed on the website field (see the FAQ above for the chained workflow).

Building the Search URL

This is the trick that makes Google Maps scraping straightforward: every Maps search is fully encoded in the URL. You build the search interactively in the Maps UI, then copy the URL.

  1. Open google.com/maps in a browser.
  2. Type your category — plumbers, dentists, law firms — and submit.
  3. Pan and zoom to the geographic area you want to cover.
  4. Once the result list looks right, copy the URL from the browser bar.

You will get something like:

https://www.google.com/maps/search/plumbers/@30.2672,-97.7431,13z

The structure is /maps/search/<query>/@<lat>,<lng>,<zoom>z. The @ block is the viewport. The zoom level controls how dense the result set is — 13z is a single neighborhood, 11z is a city, 9z is a metro. For lead-gen scraping, 12z14z per slice is the sweet spot: tight enough that Maps returns local-relevant results, wide enough that one query covers a useful area.

Three example URLs to test with:

https://www.google.com/maps/search/plumbers/@30.2672,-97.7431,13z
https://www.google.com/maps/search/law+firms/@53.4808,-2.2426,12z
https://www.google.com/maps/search/dentists/@43.6532,-79.3832,12z

(Austin TX plumbers, Manchester UK law firms, Toronto dentists.)

The API Call

Every LogPose Google Maps endpoint is asynchronous — submit a job, poll until done, fetch the result. Submit with curl first to confirm your URL works:

curl -G "https://api.logposervices.com/api/v1/ecommerce/googlemaps/search" \
  -H "X-API-Key: lp_xxxxxxx" \
  --data-urlencode "url=https://www.google.com/maps/search/plumbers/@30.2672,-97.7431,13z" \
  --data-urlencode "pages=5"
# → {"job_id": "gm_8f3a..."}

curl -H "X-API-Key: lp_xxxxxxx" \
  "https://api.logposervices.com/api/v1/jobs/gm_8f3a?wait=true&timeout=60"

curl -H "X-API-Key: lp_xxxxxxx" \
  https://api.logposervices.com/api/v1/jobs/gm_8f3a/result

Google Maps returns about 20 results per page, so pages=5 is roughly 100 leads from one viewport. Most 5-page jobs finish in 60–90 seconds.

The Python Pipeline

This is the script most teams end up running on a cron. It takes one search URL, pulls 5 pages, and writes a CSV with the columns your SDRs actually care about.

import os, time, csv, requests

API_KEY = os.environ["LOGPOSE_API_KEY"]
BASE = "https://api.logposervices.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}


def submit_and_wait(path: str, params: dict, timeout_s: int = 120) -> dict:
    r = requests.get(f"{BASE}/{path}", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    job_id = r.json()["job_id"]
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
        if s["status"] == "completed":
            break
        if s["status"] == "failed":
            raise RuntimeError(s.get("error", "unknown failure"))
        time.sleep(2)
    else:
        raise TimeoutError(f"job {job_id} did not finish in {timeout_s}s")
    return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()


def scrape_to_csv(search_url: str, pages: int, out_path: str) -> int:
    data = submit_and_wait(
        "ecommerce/googlemaps/search",
        {"url": search_url, "pages": pages},
    )
    rows = data["listings"]

    with open(out_path, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(
            f,
            fieldnames=["name", "phone", "website", "rating", "reviews", "address"],
            extrasaction="ignore",
        )
        w.writeheader()
        for r in rows:
            w.writerow(r)
    return len(rows)


if __name__ == "__main__":
    n = scrape_to_csv(
        "https://www.google.com/maps/search/plumbers/@30.2672,-97.7431,13z",
        pages=5,
        out_path="austin_plumbers.csv",
    )
    print(f"wrote {n} leads")

Run it once and you have a clean lead list. Run it nightly with a different category, and you are building a multi-niche pipeline.

Cleaning the Output for Sales Use

Raw output from one 5-page job is usually 80–110 rows. Three cleaning steps make it sales-ready:

import pandas as pd

df = pd.DataFrame(data["listings"])

# 1. Normalize phone to a stable key (digits only)
df["phone_key"] = df["phone_raw"].fillna("").astype(str)

# 2. Drop rows with no phone and no website — useless for outbound
df = df[(df["phone_key"].str.len() >= 10) | (df["website"].notna())]

# 3. Dedupe by phone first, then by Google's cid as fallback
df = df.drop_duplicates(subset="phone_key", keep="first")

# 4. Quality filter — businesses with at least a handful of reviews are
#    overwhelmingly more likely to still be operating
df = df[df["reviews"].fillna(0) >= 3]

# 5. Sort by rating desc within neighborhood for SDR-friendly review order
df = df.sort_values(["neighborhood", "rating"], ascending=[True, False])

df.to_csv("austin_plumbers_clean.csv", index=False)

The reviews >= 3 filter is the single highest-leverage cleanup step. Zero-review listings are disproportionately closed businesses, ghost listings, and Google duplicates that the deduper missed. Dropping them removes about 15% of rows and 80% of the bad-data complaints from your sales team.

Scaling Beyond a Single Viewport

One viewport gives you a neighborhood. To cover an entire metro, slice geographically and chain calls. Two patterns work in production.

Grid cover. Pick the center lat/lng for each major neighborhood in your target city, run one 5-page scrape per centroid, then merge and dedupe by cid. For Austin TX, that might be downtown, south, east, north, and the suburbs (Round Rock, Cedar Park, Pflugerville). Five viewports × 100 results × 80% dedupe yield ≈ 400 unique businesses for one category.

Bulk submission. Instead of running calls sequentially in Python, submit the whole grid in one bulk request and let the platform schedule them across the proxy pool:

requests.post(
    "https://api.logposervices.com/api/v1/ecommerce/googlemaps/search/bulk",
    headers={"X-API-Key": os.environ["LOGPOSE_API_KEY"]},
    json={
        "targets": [
            {"url": "https://www.google.com/maps/search/plumbers/@30.2672,-97.7431,13z", "pages": 5},
            {"url": "https://www.google.com/maps/search/plumbers/@30.5083,-97.6789,13z", "pages": 5},
            {"url": "https://www.google.com/maps/search/plumbers/@30.4394,-97.7733,13z", "pages": 5},
        ],
    },
).raise_for_status()

Bulk runs in parallel up to your concurrency cap, which cuts a 10-viewport grid from 15 minutes sequential to 2–3 minutes wall-clock.

For weekly net-new-listing alerts on a saved set of viewports, see the diff-loop pattern documented for the Yellow Pages equivalent — the same logic applies to Maps if you key on cid. Full walkthrough in How to monitor Yellow Pages for new businesses in your category.

Legality and Ethics

Google Maps business data is public and indexed by every search engine. Scraping it for B2B lead generation is on settled legal ground in the US (CFAA does not apply to public data per hiQ v. LinkedIn) and is broadly compliant in the EU under GDPR's legitimate-interest basis for B2B contact data — provided your downstream outreach respects the directives that actually regulate marketing communications. Cold-call regulation varies by state and country; cold-email regulation under CAN-SPAM, CASL, and GDPR is the part that requires the real legal review. The scrape is not the risky step.

Common Mistakes

  • Pasting the wrong URL form. Google Maps has three URL shapes: /maps/place/..., /maps/search/..., and short goo.gl/maps/... links. Only /maps/search/... is what the search endpoint expects; use /place URLs against the place endpoint instead.
  • Setting the zoom too wide. @30.27,-97.74,9z covers all of central Texas and returns a sparse, low-relevance result set. Stay at 12z14z per viewport.
  • Skipping geographic slicing. Running a single 50-page job at 10z returns 100 results because Maps caps the result density, not 1,000 — that is why you grid-cover instead.
  • Treating reviews=0 as a signal of a new business. It is more often a signal of a closed or duplicate listing. Filter it out.
  • Ignoring the Cloudflare 100-second edge timeout. api.logposervices.com sits behind Cloudflare, so a job that takes 100+ seconds returns a 524 to your client even though the job continues server-side. Always poll for status; never expect a synchronous response on a big page count.

Get Started

  1. Sign up at logposervices.com and generate an API key under Tool → API Keys.
  2. export LOGPOSE_API_KEY=lp_xxxxxxx
  3. Pick a search URL from the Maps UI and run the Python script above against it.

Related reading: How to build a B2B lead list from Yellow Pages (no code) for when the website field isn't critical, How to monitor Yellow Pages for new businesses for the recurring-refresh pattern, and the web scraping API guide for the broader DIY-vs-managed comparison.

External: Google Maps, Hunter.io email enrichment, hiQ Labs v. LinkedIn.

Frequently asked questions

Is it legal to scrape Google Maps business listings?
Google Maps business listings are public data — name, address, phone, website, hours, and reviews are displayed without authentication to anyone visiting the page. Scraping public web data is not a CFAA violation in the US (hiQ Labs v. LinkedIn, 9th Cir. 2022), and EU/UK precedent treats public business contact information as lawful to collect under legitimate interest. What Google's Terms of Service forbid is automated access to the underlying Google APIs without a key, and republishing the data as a competing product. For lead generation — pulling phone numbers and websites into your own CRM — the scrape itself is on solid ground; the downstream cold-outreach compliance is where the actual legal work sits.
Why not just use the Google Places API for lead generation?
Google Places is metered per-request and per-field, restricts results to 60 per query (3 pages of 20), and the Terms of Service prohibit storing most fields beyond 30 days or using them outside a Google Maps-integrated experience. For a one-off lead build that needs phone + address + website with no further processing, Places works. For a recurring weekly refresh feeding your sales CRM, the terms-of-service restrictions effectively rule it out — which is why every serious B2B lead vendor either scrapes Maps directly or licenses data from a Google-approved aggregator.
What fields does Google Maps return per business?
Search results return name, full address plus parsed address components, primary category, all assigned categories, neighborhood, latitude/longitude, rating, review count, phone (both formatted and raw), website URL and display label, timezone, full weekly hours keyed by day, and Google's internal identifiers (feature_id, cid, data_id). The place-detail endpoint adds richer fields including the photo gallery and review text. Crucially, Google Maps returns a website URL for the majority of small businesses — that is the field Yellow Pages is weakest on, and the reason most lead programs end up scraping both.
How many Google Maps results can I get per search?
Google Maps caps a single search-URL pagination at around 100–120 results before the result density drops sharply (Google starts returning low-relevance fillers from neighboring areas). For wider coverage, slice your search geographically — instead of one query for `dentists in Toronto`, run separate queries for each neighborhood (downtown, North York, Etobicoke, Scarborough) and dedupe by phone or by the Google `cid` identifier. The LogPose Google Maps endpoint accepts `pages` up to 500, but in practice 5–10 pages per geo-slice is the sweet spot before duplicates dominate.
Can I get email addresses for businesses from Google Maps?
No. Neither Google Maps nor Yellow Pages publish email addresses — both surface phone and website only. To get a decision-maker email you chain a second step: take the website domain from the Google Maps result, run it through an email-discovery API (Hunter, Apollo, FindThatLead, Snov), and accept that the match rate will be 30–60% depending on the niche. For local services (plumbers, dentists, contractors), the practical playbook is to dial the phone first and capture email during the call — most lead programs over-invest in finding emails when the phone number is the actually-useful identifier.

Related posts

Tutorial

How to Monitor Yellow Pages for New Businesses in Your Category

9 min read
Tutorial

How to Build a B2B Lead List from Yellow Pages (No Code)

9 min read
Comparison

Octoparse Alternatives for Lead Generation (No-Code & API)

9 min read