Purpose
Extract structured information from a Google Maps place listing: canonical URL, place identity, address, contact and category details, rating/review count, visible reviews, and photo/video image URLs. The workflow is read-only and must not submit reviews, ratings, edits, forms, or contact actions. Place names may be resolved through a direct Maps search URL; opaque place identifiers must always be read from Google's result URL rather than guessed.
When to Use
- Scraping reviews and listing information from a supplied Google Maps place URL.
- Collecting visible business photos or user-uploaded images associated with a place.
- Resolving a place name, landmark, station, or address to the correct Google Maps listing.
- Building a read-only place dataset containing identity, address, rating, reviews, and media.
- Checking the location or nearby context of a named place in Google Maps.
Workflow
If the caller supplies a canonical Google Maps place URL, navigate to it directly. Remove analytics-only parameters such as
entry,g_ep, andskidwhen desired, but preserve the/maps/place/path, coordinates, and opaque place identifier in/data/or!1s...segments. Do not visit the Google homepage.If only
{place-name}is supplied, use one direct Maps search navigation:
https://www.google.com/maps/search/?api=1&query={url-encoded-place-name}Google also accepts the shorter path form:
https://www.google.com/maps/search/{url-encoded-place-name}For an area-qualified request, include the locality or street in the query, for example {place-name} {area-or-address}. After Maps renders, inspect anchors containing /maps/place/, select the result whose visible name, locality, and place type best match the request, read its complete href, and navigate directly to that href. Never fabricate an opaque Maps identifier from a name or coordinates. If the result URL already contains /@{latitude},{longitude},..., preserve those coordinates as resolved evidence, not as reusable constants.
- In one browser-agent call, navigate with
waitUntil: "domcontentloaded", wait briefly for Maps rendering, then evaluate this extractor on the initial place page:
(()=>{const clean=s=>(s||'').replace(/\\s+/g,' ').trim();const href=location.href,u=new URL(href),body=clean(document.body?.innerText),main=document.querySelector('[role="main"]')||document.body;const title=clean(document.querySelector('h1')?.textContent)||clean(main.querySelector('[aria-label*="About "]')?.getAttribute('aria-label')?.replace(/^About\\s+/i,''))||null;const aria=[...document.querySelectorAll('[aria-label]')].map(e=>clean(e.getAttribute('aria-label'))).filter(Boolean);const address=aria.find(x=>/^Address:/i.test(x))?.replace(/^Address:\\s*/i,'')||clean(document.querySelector('[data-item-id^="address"]')?.textContent)||clean([...document.querySelectorAll('button')].find(b=>/address/i.test(b.getAttribute('data-item-id')||''))?.textContent)||null;const ratingLabel=aria.find(x=>/stars?/i.test(x)&&/[0-5](?:\\.[0-9])?/i.test(x));const rating=ratingLabel?Number((ratingLabel.match(/[0-5](?:\\.[0-9])?/)||[])[0]):Number((body.match(/\\b([0-5]\\.[0-9])\\s*\\(([^)]+)\\)/)||[])[1])||null;const reviewCount=Number((body.match(/\\(([\\d,.]+)\\)/)||[])[1]?.replace(/[,.]/g,''))||null;const info={};for(const b of document.querySelectorAll('button[data-item-id],button[aria-label],[data-item-id]')){const label=clean(b.getAttribute('aria-label')||b.getAttribute('data-item-id')),text=clean(b.textContent);if(label&&text)info[label]=text}const reviews=[...document.querySelectorAll('[data-review-id]')].map(card=>{const text=clean(card.querySelector('.wiI7pd,[lang]')?.textContent)||clean(card.innerText),author=clean(card.querySelector('.d4r55,[class*="d4r55"]')?.textContent)||null,ratingText=clean(card.querySelector('[role="img"][aria-label*="star" i]')?.getAttribute('aria-label'))||'',date=clean(card.querySelector('.rsqaWe,[class*="rsqaWe"]')?.textContent)||null;return{reviewId:card.getAttribute('data-review-id'),author,rating:Number((ratingText.match(/[0-5](?:\\.[0-9])?/)||[])[0])||null,date,text,raw:clean(card.innerText)}}).filter(r=>r.text||r.raw);const images=[...document.querySelectorAll('a[aria-label^="Photo"],a[aria-label="Video"],img')].map((e,i)=>{const img=e.matches('img')?e:e.querySelector('img');return{index:i+1,type:e.getAttribute('aria-label')==='Video'?'video':'photo',label:clean(e.getAttribute('aria-label')),href:e.href||null,src:img?.currentSrc||img?.src||null,alt:clean(img?.alt)}}).filter(x=>x.href||x.src);const unique=a=>[...new Map(a.map(x=>[(x.href||x.src||JSON.stringify(x)),x])).values()];const blocked=/consent|sign in|captcha|unusual traffic|verify you are human/i.test(body)&&!title;const coord=(href.match(/@(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)/)||[]);return{url:href,isGoogleMaps:/((^|\\.)google\\.com)$/.test(u.hostname)&&u.pathname.startsWith('/maps'),blocked,place:title,address,rating:Number.isFinite(rating)?rating:null,reviewCount,info,reviews:unique(reviews),images:unique(images),coordinates:coord[1]?{latitude:Number(coord[1]),longitude:Number(coord[2])}:null,bodyExcerpt:body.slice(0,2000)}})()If complete reviews are requested and the initial page exposes
button[aria-label^="More reviews"], click it, wait for the reviews panel, and rerun the review portion of the evaluator against[data-review-id]cards. Deduplicate bydata-review-id; return only reviews actually rendered, not an invented total. Use visible sort/filter controls only when requested and when no stable URL equivalent is available.If the full photo gallery is requested, click the visible photo gallery control or
button[aria-label^="Photo of "], wait for the gallery, and rerun the image portion of the evaluator. Prefer anchorhreforcurrentSrc; retain the displayed thumbnail URL when no larger link is present.Report the canonical final URL, explicit visible fields, resolved coordinates when present, and unavailable fields as
nullor empty arrays. Do not infer business facts from coordinates, map tiles, image filenames, or review-count text alone.
Site-Specific Gotchas
- Direct
/maps/search/?api=1&query=...navigation is the shortest general name-resolution route; no homepage visit or search-box typing is needed. - The observed path form
/maps/search/{encoded-query}also resolves area-qualified place searches, but theapi=1&query=form is preferred for reusable recipes. - Google Maps place pages contain opaque identifiers such as
0x...:0x...CIDs and/g/{id}identifiers. Preserve identifiers discovered in result URLs; never guess them. - Search result hrefs may include a human-readable name,
/@lat,lngcoordinates, and opaque identifiers. Select by visible name and locality, not by the first URL alone when several similarly named places exist. - The place overview may require a short render wait after
domcontentloaded. A successful navigation does not mean the details panel, reviews, or photos has rendered. - Review cards commonly use
[data-review-id], with text in.wiI7pdand author text in.d4r55; selectors are dynamic and should be treated as fallbacks. - Photo links commonly use
a[aria-label^="Photo"]; gallery videos may usea[aria-label="Video"]. Distinguish videos from still photos in output. - Do not scrape
googleusercontent.comseparately as a second navigation. Only return image URLs already exposed in the current Maps DOM. - Google may show consent, login, CAPTCHA, or an unusual-traffic interstitial. Return
blocked: trueand no fabricated place data if the listing panel is absent. - Map tile
/maps/vt/pbrequests are implementation details and are not place data; do not call or parse them directly. - Session-specific parameters such as
entry,skid,g_st, and long analytics payloads are not needed in reusable URLs. - Listing details, reviews, photos, and coordinates are time-dependent and may vary by locale or session; report only what is currently rendered.
Expected Output
{
"url": "https://www.google.com/maps/place/{place}/...",
"place": "{place name}",
"address": "{visible address}",
"coordinates": {"latitude": 0, "longitude": 0},
"rating": 4.2,
"reviewCount": 150,
"info": {"Category": "...", "Phone": "...", "Website": "..."},
"reviews": [
{"reviewId": "...", "author": "...", "rating": 5, "date": "...", "text": "...", "raw": "..."}
],
"images": [
{"index": 1, "type": "photo", "label": "Photo of ...", "href": "...", "src": "...", "alt": "..."}
],
"blocked": false,
"bodyExcerpt": "..."
}If Maps is blocked or the requested section did not render, return the URL, blocked: true or a section-specific empty array, and a concise reason.