Local SEO Map Pack & GMB Scraping
Local search data lives in three places: the SERP Local Pack, Google Maps, and Google Business Profile listings. This guide shows what to extract, how Maps blocks scrapers, and working Playwright code that pipes through mobile proxies.
Local SEO data comes from three sources on Google: the Local Pack that appears in a regular SERP (3 results with a small map), Google Maps itself (unlimited scroll, dozens of businesses per area), and individual Google Business Profile listings (formerly "GMB" — the full record including reviews, photos, posts, and Q&A). Tools like Local Falcon, BrightLocal, and PlePer scrape all three, then cross-reference for rank tracking across grids.
1. Data You Can Extract
Core business fields
- • Business name
- • Primary category (and subcategories)
- • Rating (0-5, 1 decimal)
- • Review count (total)
- • Full street address
- • Phone number
- • Website URL
- • Opening hours (per day of week)
- • Price level ($, $$, $$$, $$$$)
Rich fields (Maps & GMB)
- • Photo count (and most recent upload date)
- • Reviews — latest, most relevant, lowest, highest
- • Review replies from owner
- • Q&A section (user questions + answers)
- • Google Posts (offers, updates, events)
- • Attributes (wheelchair accessible, WiFi, etc.)
- • Popular times (histogram by hour)
- • Related searches ("people also search for")
- • CID / feature ID (stable Google identifier)
2. Why Google Maps Is Harder Than Regular SERP
Maps is more aggressive than google.com/search on bot detection for a simple reason: the data has monetary value (lead-gen scrapers, competitor-review harvesters, data brokers reselling business directories). Three practical differences:
- →JavaScript-rendered: Maps is a single-page app. A plain
requests.get()returns a shell with no business data. You need a real browser. - →Datacenter blocks hit faster: Maps blocks AWS, GCP, and Hetzner IPs often on the first query. Residential IPs tolerate maybe 30-60 searches per hour. Mobile IPs comfortably hold 100+.
- →Consent + location dialogs: in EU geos, a CONSENT cookie dialog intercepts the first visit. Script it or pre-set the cookie.
Proxy strategy that works: sticky session per search query (so pagination and review expansion stay on one IP), then rotate the IP between different searches. Mobile proxies at buy.mobileproxies.org expose rotation via API — call the rotate endpoint between queries.
3. Playwright Map Pack Scraper
Playwright drives a real Chromium instance, which is necessary because Maps renders everything in JavaScript. The mobile proxy is configured at the browser level so all traffic (including the XHR calls Maps makes internally) routes through the carrier IP.
from playwright.sync_api import sync_playwright
from urllib.parse import quote_plus
import time
proxy_config = {
"server": "http://proxy.mobileproxies.org:8000",
"username": "your-username",
"password": "your-password",
}
def scrape_map_pack(query, location):
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy=proxy_config,
)
context = browser.new_context(
locale="en-US",
user_agent=(
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 "
"Mobile/15E148 Safari/604.1"
),
)
# Pre-set CONSENT to skip the EU cookie banner.
context.add_cookies([{
"name": "CONSENT", "value": "YES+1", "domain": ".google.com", "path": "/",
}])
page = context.new_page()
url = f"https://www.google.com/maps/search/{quote_plus(query + ' ' + location)}"
page.goto(url, wait_until="domcontentloaded", timeout=30000)
page.wait_for_selector("div[role='article']", timeout=15000)
# Scroll the results feed to load more businesses.
feed = page.locator("div[role='feed']")
for _ in range(5):
feed.evaluate("el => el.scrollBy(0, el.scrollHeight)")
time.sleep(1.5)
businesses = []
for card in page.locator("div[role='article']").all():
name = card.locator("div.fontHeadlineSmall").inner_text(timeout=2000) if card.locator("div.fontHeadlineSmall").count() else None
rating = card.locator("span[role='img']").get_attribute("aria-label") if card.locator("span[role='img']").count() else None
link = card.locator("a.hfpxzc").get_attribute("href") if card.locator("a.hfpxzc").count() else None
if name:
businesses.append({"name": name, "rating": rating, "url": link})
browser.close()
return businesses
if __name__ == "__main__":
results = scrape_map_pack("coffee shop", "Austin TX")
for r in results[:10]:
print(r)
For each business card you get the visible list-view data. To pull reviews, hours, and Q&A, click the card (card.click()) and wait for the detail panel — then parse a second set of selectors.
4. Scraping Reviews at Scale
Reviews have two sort orders that return different data — "Most relevant" (Google's ranking) and "Newest" (reverse chronological). For review-monitoring you want newest, polled daily per business. For sentiment snapshots, most-relevant is fine.
- →Click the star-rating row to open the full reviews panel
- →Select the sort dropdown and pick "Newest"
- →Scroll the review pane until the last-seen review appears — stop and save the cursor
- →Each review row contains reviewer name, rating, relative timestamp, text, owner reply
Google's place detail pages load reviews via a batched endpoint — for very large review volumes, intercept network requests in Playwright and grab the raw JSON response instead of re-parsing the DOM on every scroll.
5. Handling Maps Infinite Scroll
Maps uses a virtualized results feed — scrolling triggers more business cards to render. The end-of-results signal is the text "You've reached the end of the list." appearing at the bottom. Practical loop:
feed = page.locator("div[role='feed']")
previous_count = 0
for _ in range(30): # hard cap
feed.evaluate("el => el.scrollBy(0, el.scrollHeight)")
page.wait_for_timeout(1500)
current_count = page.locator("div[role='article']").count()
if current_count == previous_count:
break # no new results loaded
previous_count = current_count
if page.locator("text=You've reached the end").count():
break
Related Guides
Mobile Proxies for Google Maps
Sticky-session mobile IPs in every major US + EU carrier. Clean enough for Maps, cheap enough to scale.