{
  "version": "1.0.0",
  "exported_at": "2026-05-31T06:15:00.000Z",
  "project": {
    "name": "Google Maps Listings Scraper by Keywords",
    "description": "Scrapes Google Maps keyword search listing results. Default query is 'petrol station in london'; edit the navigate.urls array to add more keyword/location searches. Pagination/navigation is handled with bounded Google Maps infinite-scroll JavaScript: it repeatedly scrolls the results feed, collects each visible listing card into a hidden export buffer, stops when the end marker appears or scrolling stabilizes/max scrolls are reached, then exports all collected rows. Best-effort template: Google Maps may change selectors, show consent/CAPTCHA, or limit results to roughly 120-200 per query.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 100,
      "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": 460,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.google.com/maps/search/petrol+station+in+london?hl=en"
        ],
        "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": 820,
      "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": 1180,
      "position_y": 220,
      "config": {
        "selector": "div[role=\"feed\"] div.Nv2PK, div[role=\"feed\"] div[role=\"article\"]",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript on the page",
      "position_x": 1540,
      "position_y": 220,
      "config": {
        "waitForCompletion": true,
        "timeout": 120,
        "jsCode": "(async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const feed = document.querySelector('div[role=\"feed\"]') || document.querySelector('[aria-label*=\"Results for\"]');\n  if (!feed) return 'Google Maps results feed not found';\n\n  let store = document.querySelector('#uscraper-gmaps-store');\n  if (!store) {\n    store = document.createElement('div');\n    store.id = 'uscraper-gmaps-store';\n    store.style.display = 'none';\n    document.body.appendChild(store);\n  }\n\n  const clean = (value) => String(value || '').replace(/\\s+/g, ' ').trim();\n  const keyword = clean(decodeURIComponent((location.pathname.split('/maps/search/')[1] || '').split('/')[0]).replaceAll('+', ' ') || new URLSearchParams(location.search).get('q') || '');\n  const seen = new Set(Array.from(store.children).map((el) => el.getAttribute('data-key')));\n\n  function collectCards() {\n    const cards = Array.from(feed.querySelectorAll('div.Nv2PK, div[role=\"article\"]')).filter((card) => {\n      return card.querySelector('a.hfpxzc') || clean(card.innerText).length > 30;\n    });\n\n    for (const ROW of cards) {\n      const detailUrl = ROW.querySelector('a.hfpxzc')?.href || '';\n      const name = clean(ROW.querySelector('a.hfpxzc')?.getAttribute('aria-label') || ROW.querySelector('.qBF1Pd')?.textContent || ROW.querySelector('[role=\"heading\"]')?.textContent || '');\n      if (!detailUrl && !name) continue;\n\n      const key = detailUrl || `${name}|${clean(ROW.innerText).slice(0, 120)}`;\n      if (seen.has(key)) continue;\n      seen.add(key);\n\n      const text = ROW.innerText || '';\n      const flatText = clean(text);\n      const parts = text.split('·').map((part) => clean(part)).filter(Boolean);\n      const rating = clean(ROW.querySelector('.MW4etd')?.textContent || ((flatText.match(/\\b\\d\\.\\d\\b/) || [])[0] || ''));\n      const reviewCount = clean((ROW.querySelector('.UY7F9')?.textContent || ((flatText.match(/\\(([^)]+)\\)/) || [])[1] || '')).replace(/[()]/g, ''));\n      const priceRange = clean(((flatText.match(/(^|\\s)(\\${1,4})(\\s|$)/) || [])[2] || ''));\n      const category = clean(parts.find((part) => /station|restaurant|store|hotel|clinic|shop|service|company|contractor|agency|bar|cafe|garage|pharmacy|dealer|market|business/i.test(part) && !/^\\$/.test(part) && !/reviews?/i.test(part)) || '');\n      const address = clean(parts.find((part) => /\\d+\\s+/.test(part) && !/[★()]/.test(part) && !/reviews?/i.test(part) && !/Open|Closed|Closes|Opens/i.test(part)) || '');\n      const coordMatch = detailUrl.match(/!3d(-?\\d+\\.\\d+)!4d(-?\\d+\\.\\d+)/) || detailUrl.match(/@(-?\\d+\\.\\d+),(-?\\d+\\.\\d+)/);\n      const latitude = coordMatch ? coordMatch[1] : '';\n      const longitude = coordMatch ? coordMatch[2] : '';\n      const statusMatch = flatText.match(/\\b(Open 24 hours|Open|Closed|Temporarily closed|Permanently closed)\\b/i);\n      const currentStatus = statusMatch ? clean(statusMatch[0]) : '';\n      const nextMatch = flatText.match(/(Closes [^·\\n]+|Opens [^·\\n]+)/i);\n      const nextStatus = nextMatch ? clean(nextMatch[0]) : '';\n      const websiteEl = ROW.querySelector('a[data-value=\"Website\"], a[aria-label*=\"Website\"], a[href^=\"http\"]:not(.hfpxzc)');\n      const website = websiteEl ? websiteEl.href : '';\n      const telEl = ROW.querySelector('a[href^=\"tel:\"]');\n      const telephone = clean(telEl ? telEl.href.replace('tel:', '') : ((flatText.match(/(\\+?\\d[\\d\\s().-]{7,}\\d)/) || [])[1] || ''));\n      const tags = Array.from(ROW.querySelectorAll('.W4Efsd span, .fontBodyMedium span')).map((el) => clean(el.textContent)).filter((tag) => tag && /\\$|Open|Closed|Delivery|Takeout|Wheelchair|Diesel|Regular|Premium|Service|Gas|Fuel/i.test(tag)).slice(0, 10).join(' | ');\n\n      const row = document.createElement('div');\n      row.className = 'uscraper-gmaps-row';\n      const data = {\n        key,\n        keyword,\n        name,\n        rating,\n        reviewCount,\n        priceRange,\n        category,\n        address,\n        latitude,\n        longitude,\n        currentStatus,\n        nextStatus,\n        website,\n        scrapedAt: new Date().toISOString(),\n        telephone,\n        tags,\n        detailUrl\n      };\n      for (const [field, value] of Object.entries(data)) {\n        row.setAttribute('data-' + field.replace(/[A-Z]/g, (match) => '-' + match.toLowerCase()), value);\n      }\n      store.appendChild(row);\n    }\n    return seen.size;\n  }\n\n  let lastScrollTop = -1;\n  let lastScrollHeight = -1;\n  let stableScrolls = 0;\n\n  for (let i = 0; i < 45; i++) {\n    collectCards();\n    const feedText = feed.innerText || '';\n    if (/reached the end of the list|no more results/i.test(feedText)) break;\n\n    const beforeTop = feed.scrollTop;\n    const beforeHeight = feed.scrollHeight;\n    feed.scrollTop = feed.scrollHeight;\n    feed.dispatchEvent(new WheelEvent('wheel', { deltaY: 2500, bubbles: true }));\n    await sleep(1500);\n\n    if (feed.scrollTop === lastScrollTop && feed.scrollHeight === lastScrollHeight && beforeTop === feed.scrollTop && beforeHeight === feed.scrollHeight) {\n      stableScrolls += 1;\n    } else {\n      stableScrolls = 0;\n    }\n    lastScrollTop = feed.scrollTop;\n    lastScrollHeight = feed.scrollHeight;\n    if (stableScrolls >= 5) break;\n  }\n\n  collectCards();\n  return `Collected ${seen.size} Google Maps listings`;\n})();"
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1900,
      "position_y": 220,
      "config": {
        "selector": "#uscraper-gmaps-store .uscraper-gmaps-row",
        "timeout": 20,
        "visible": false
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2260,
      "position_y": 220,
      "config": {
        "rowSelector": "#uscraper-gmaps-store .uscraper-gmaps-row",
        "fileName": "google-maps-scraper-listing-page-by-keyword.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "keyword",
            "selector": "",
            "attribute": "data-keyword"
          },
          {
            "name": "name",
            "selector": "",
            "attribute": "data-name"
          },
          {
            "name": "rating",
            "selector": "",
            "attribute": "data-rating"
          },
          {
            "name": "review_count",
            "selector": "",
            "attribute": "data-review-count"
          },
          {
            "name": "price_range",
            "selector": "",
            "attribute": "data-price-range"
          },
          {
            "name": "category",
            "selector": "",
            "attribute": "data-category"
          },
          {
            "name": "address",
            "selector": "",
            "attribute": "data-address"
          },
          {
            "name": "latitude",
            "selector": "",
            "attribute": "data-latitude"
          },
          {
            "name": "longitude",
            "selector": "",
            "attribute": "data-longitude"
          },
          {
            "name": "current_status",
            "selector": "",
            "attribute": "data-current-status"
          },
          {
            "name": "next_status",
            "selector": "",
            "attribute": "data-next-status"
          },
          {
            "name": "website",
            "selector": "",
            "attribute": "data-website"
          },
          {
            "name": "scraped_at",
            "selector": "",
            "attribute": "data-scraped-at"
          },
          {
            "name": "telephone",
            "selector": "",
            "attribute": "data-telephone"
          },
          {
            "name": "tags",
            "selector": "",
            "attribute": "data-tags"
          },
          {
            "name": "detail_url",
            "selector": "",
            "attribute": "data-detail-url"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2620,
      "position_y": 220,
      "config": {}
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 2980,
      "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": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "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"
    },
    {
      "from_block_id": "loop-continue-1",
      "from_connector_id": "right",
      "to_block_id": "end-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 28,
      "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": 388,
      "position_y": 116,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1468,
      "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": 2188,
      "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": 2548,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 2908,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Google Maps keyword search listing results. Default query is 'petrol station in london'; edit the navigate.urls array to add more keyword/location searches. Pagination/navigation is handled with bounded Google Maps infinite-scroll JavaScript: it repeatedly scrolls the results feed, collects each visible listing card into a hidden export buffer, stops when the end marker appears or scrolling stabilizes/max scrolls are reached, then exports all collected rows. Best-effort template: Google Maps may change selectors, show consent/CAPTCHA, or limit results to roughly 120-200 per query.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const fe...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1740,
      "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": "Extracts rows matching `#uscraper-gmaps-store .uscraper-gmaps-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2460,
      "position_y": 200,
      "width": 340,
      "height": 118,
      "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": 2820,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}