{
  "version": "1.0.0",
  "exported_at": "2026-06-03T10:15:00.000Z",
  "project": {
    "name": "Google Maps Listing and Details Page Scraper Cloud",
    "description": "Best-effort Google Maps place detail scraper equivalent to the Octoparse template. It loops through all Google Maps place URLs supplied in navigate.urls and appends one CSV row per place, extracting name, image, rating, reviews, price/category, address, status, hours when visible, coordinates, plus code, website, phone, and review keywords when visible. Keyword search/listing discovery is not automated because UScraper templates cannot pipe discovered Google Maps listing URLs into later navigation blocks; add specific Google Maps place URLs to the urls array. Google Maps may throttle, change DOM classes, or show consent/CAPTCHA pages.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated-google-maps-listing-details-cloud"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser viewport size",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.google.com/maps/place/Starbucks/data=!4m7!3m6!1s0x808e33b4dce2b6c7:0x756ee5a39ae8972d!8m2!3d37.274881!4d-121.857555!16s%2Fg%2F1vfp8fl4!19sChIJx7bi3LQzjoARLZfomqPlbnU?authuser=0&hl=en&rclk=1"
        ],
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "selector": "h1.DUwDvf, h1",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "jsCode": "(() => { const accept = Array.from(document.querySelectorAll('button')).find(b => /^(accept|accept all|i agree)$/i.test((b.innerText || b.getAttribute('aria-label') || '').trim())); if (accept) { try { accept.click(); } catch (e) {} } const panes = Array.from(document.querySelectorAll('div[role=\"main\"], .m6QErb, [aria-label^=\"Information for\"]')); panes.forEach(p => { try { p.scrollTop = Math.max(p.scrollTop, 1400); } catch (e) {} }); window.scrollTo(0, Math.min(document.body.scrollHeight, 1600)); return true; })();",
        "waitForCompletion": true,
        "timeout": 10
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "rowSelector": "body",
        "fileName": "google-maps-listing-and-details-page-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "keyword",
            "selector": "(() => { const params = new URLSearchParams(location.search); if (/\\/maps\\/search\\//.test(location.pathname)) return decodeURIComponent(location.pathname.split('/maps/search/')[1] || '').split('/')[0].replace(/\\+/g, ' '); return params.get('q') || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "name",
            "selector": "(() => ROW.querySelector('h1.DUwDvf')?.innerText?.trim() || Array.from(ROW.querySelectorAll('h1')).map(h => h.innerText.trim()).find(t => t && !/^(Hours|Menu|Reviews|About)$/i.test(t)) || document.title.replace(/ - Google Maps$/,'').trim())()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "header_image",
            "selector": "(() => { const imgs = Array.from(ROW.querySelectorAll('button[aria-label^=\"Photo of\"] img, .ZKCDEc img, img[src*=\"googleusercontent.com\"]')); const urls = imgs.map(img => img.currentSrc || img.src || '').filter(Boolean); let best = urls.find(u => /=w(?!32\\b)\\d+-h(?!32\\b)\\d+/i.test(u)) || urls.find(u => /googleusercontent\\.com/i.test(u)) || ''; if (best) best = best.replace(/=w\\d+-h\\d+(?:-[a-z0-9]+)*$/i, '=w408-h306-k-no'); return best; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "rating",
            "selector": "(() => { const box = ROW.querySelector('.F7nice'); const text = box?.innerText || ROW.innerText; const m = text.match(/\\b\\d+(?:\\.\\d+)?\\b/); return m ? m[0] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "rating_count",
            "selector": "(() => { const labels = Array.from(ROW.querySelectorAll('[aria-label]')).map(e => e.getAttribute('aria-label') || ''); const s = labels.find(v => /reviews/i.test(v)) || ROW.innerText; const m = s.match(/([\\d,]+)\\s+reviews/i); return m ? m[1].replace(/,/g,'') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "price_range",
            "selector": "(() => { const text = ROW.querySelector('.skqShb')?.innerText || ROW.innerText; const m = text.match(/\\$[\\d$–\\-\\s]+(?:per person)?|\\${1,4}/); return m ? m[0].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "category",
            "selector": "(() => ROW.querySelector('button[jsaction*=\"category\"], button.DkEaL')?.innerText?.trim() || '')()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "detail_url",
            "selector": "(() => location.href)()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "tags",
            "selector": "(() => { const vals = Array.from(ROW.querySelectorAll('.LTs0Rc')).map(e => (e.getAttribute('aria-label') || e.innerText || '').replace(/^(Serves|Has|Offers)\\s+/i,'').trim()).filter(Boolean); return Array.from(new Set(vals)).join(' · '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "address",
            "selector": "(() => { const b = ROW.querySelector('button[data-item-id=\"address\"]'); const label = b?.getAttribute('aria-label') || ''; return b?.querySelector('.Io6YTe')?.innerText?.trim() || label.replace(/^Address:\\s*/i,'').trim(); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "located_in",
            "selector": "(() => { const el = Array.from(ROW.querySelectorAll('[aria-label], .Io6YTe')).find(e => /^Located in:/i.test((e.getAttribute('aria-label') || e.innerText || '').trim())); const txt = el ? (el.getAttribute('aria-label') || el.innerText || '') : ''; return txt.replace(/^Located in:\\s*/i,'').trim(); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "current_status",
            "selector": "(() => { const b = ROW.querySelector('button[data-item-id=\"oh\"]'); const label = b?.getAttribute('aria-label') || b?.innerText || ''; const parts = label.split('·').map(s => s.trim()).filter(Boolean); return parts[0] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "next_status",
            "selector": "(() => { const b = ROW.querySelector('button[data-item-id=\"oh\"]'); const label = b?.getAttribute('aria-label') || b?.innerText || ''; const parts = label.split('·').map(s => s.trim()).filter(Boolean); return parts[1] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "scraped_at",
            "selector": "(() => new Date().toISOString())()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "open_hours",
            "selector": "(() => { const tables = Array.from(ROW.querySelectorAll('table')).map(t => t.innerText.trim().replace(/\\n+/g, ' | ')).filter(t => /(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)/i.test(t) && !/^5\\s*\\|\\s*4\\s*\\|\\s*3\\s*\\|\\s*2\\s*\\|\\s*1/i.test(t)); return tables[0] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "latitude",
            "selector": "(() => { const u = decodeURIComponent(location.href); let m = u.match(/@(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)/); if (m) return m[1]; m = u.match(/!3d(-?\\d+(?:\\.\\d+)?)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "longitude",
            "selector": "(() => { const u = decodeURIComponent(location.href); let m = u.match(/@(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)/); if (m) return m[2]; m = u.match(/!4d(-?\\d+(?:\\.\\d+)?)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "plus_code",
            "selector": "(() => { const byId = ROW.querySelector('[data-item-id=\"oloc\"], [data-item-id*=\"plus\"]'); const label = byId?.getAttribute('aria-label') || byId?.innerText || ''; if (label) return label.replace(/^Plus code:\\s*/i,'').trim(); const m = ROW.innerText.match(/\\b[23456789CFGHJMPQRVWX]{4}\\+[23456789CFGHJMPQRVWX]{2,3}[^\\n]*/); return m ? m[0].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "website",
            "selector": "(() => { const links = Array.from(ROW.querySelectorAll('a[href*=\"/url?q=\"]')); const decoded = links.map(a => { try { return new URL(a.href).searchParams.get('q') || a.href; } catch(e) { return a.href; } }).filter(u => /^https?:\\/\\//i.test(u) && !/google\\.|gstatic|support\\.google/i.test(u)); const nonMenu = decoded.find(u => !/\\/menu\\/?$/i.test(u)); return nonMenu || decoded[0] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "phone",
            "selector": "(() => { const phoneButton = Array.from(ROW.querySelectorAll('[aria-label]')).find(e => /^Phone:/i.test(e.getAttribute('aria-label') || '')); const label = phoneButton?.getAttribute('aria-label') || ''; if (label) return label.replace(/^Phone:\\s*/i,'').trim(); const tel = ROW.querySelector('a[href^=\"tel:\"]')?.getAttribute('href')?.replace(/^tel:/,'') || ''; if (tel) return tel; const m = ROW.innerText.match(/(?:\\+?1[\\s.-]?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}/); return m ? m[0] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword1",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[0] ? chips[0].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword2",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[1] ? chips[1].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword3",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[2] ? chips[2].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword4",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[3] ? chips[3].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword5",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[4] ? chips[4].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword6",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[5] ? chips[5].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword7",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[6] ? chips[6].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword8",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[7] ? chips[7].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword9",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[8] ? chips[8].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_keyword10",
            "selector": "(() => { const chips = Array.from(ROW.querySelectorAll('button')).map(b => (b.innerText || b.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ')).filter(t => /^[a-z][a-z0-9&'’ -]{1,40}\\s+\\d{1,4}$/i.test(t) && !/^(photos?|reviews?|directions|save|share|nearby|menu|overview|about|all|sort|street view|view|more)/i.test(t)); return chips[9] ? chips[9].replace(/\\s+(\\d{1,4})$/, ', mentioned in $1 reviews') : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 220,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 408,
      "position_y": 116,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1488,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2208,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2568,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Google Maps place detail scraper equivalent to the Octoparse template. It loops through all Google Maps place URLs supplied in navigate.urls and appends one CSV row per place, extracting name, image, rating, reviews, price/category, address, status, hours when visible, coordinates, plus code, website, phone, and review keywords when visible. Keyword search/listing discovery is not automated because UScraper templates cannot pipe discovered Google Maps listing URLs into later navigation blocks; add specific Google Maps place URLs to the urls array. Google Maps may throttle, change DOM classes, or show consent/CAPTCHA pages.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 1 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 680,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => { const accept = Array.from(document.querySelectorAll('button')).find(b => /^(accept|accept a...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (keyword, name, header_image, rating, rating_count). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 200,
      "width": 340,
      "height": 130,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 2840,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}