google map scraper

Google map scraper: An open-source, free tool to obtain unlimited local business examples with email addresses.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         google map scraper
// @namespace    http://google.com/
// @version      1.0.5
// @description  Google map scraper: An open-source, free tool to obtain unlimited local business examples with email addresses.
// @author       Web Automation Lover
// @match        *://*.google.com/maps/search/*/*
// @match        *://*.google.ad/maps/search/**
// @match        *://*.google.ae/maps/search/**
// @match        *://*.google.ac/maps/search/**
// @match        *://*.google.com.ag/maps/search/**
// @match        *://*.google.com.ai/maps/search/**
// @match        *://*.google.com.af/maps/search/**
// @match        *://*.google.al/maps/search/**
// @match        *://*.google.am/maps/search/**
// @match        *://*.google.co.ao/maps/search/**
// @match        *://*.google.at/maps/search/**
// @match        *://*.google.com.ar/maps/search/**
// @match        *://*.google.as/maps/search/**
// @match        *://*.google.com.au/maps/search/**
// @match        *://*.google.com.bd/maps/search/**
// @match        *://*.google.az/maps/search/**
// @match        *://*.google.ba/maps/search/**
// @match        *://*.google.bg/maps/search/**
// @match        *://*.google.be/maps/search/**
// @match        *://*.google.bf/maps/search/**
// @match        *://*.google.com.bh/maps/search/**
// @match        *://*.google.com.bn/maps/search/**
// @match        *://*.google.bi/maps/search/**
// @match        *://*.google.bj/maps/search/**
// @match        *://*.google.bs/maps/search/**
// @match        *://*.google.com.bo/maps/search/**
// @match        *://*.google.com.br/maps/search/**
// @match        *://*.google.bt/maps/search/**
// @match        *://*.google.co.bw/maps/search/**
// @match        *://*.google.by/maps/search/**
// @match        *://*.google.com.bz/maps/search/**
// @match        *://*.google.ca/maps/search/**
// @match        *://*.google.com.kh/maps/search/**
// @match        *://*.google.cc/maps/search/**
// @match        *://*.google.cd/maps/search/**
// @match        *://*.google.cf/maps/search/**
// @match        *://*.google.cat/maps/search/**
// @match        *://*.google.cg/maps/search/**
// @match        *://*.google.ch/maps/search/**
// @match        *://*.google.ci/maps/search/**
// @match        *://*.google.co.ck/maps/search/**
// @match        *://*.google.cl/maps/search/**
// @match        *://*.google.cm/maps/search/**
// @match        *://*.google.cn/maps/search/**
// @match        *://*.google.com.co/maps/search/**
// @match        *://*.google.co.cr/maps/search/**
// @match        *://*.google.com.cu/maps/search/**
// @match        *://*.google.cv/maps/search/**
// @match        *://*.google.com.cy/maps/search/**
// @match        *://*.google.cz/maps/search/**
// @match        *://*.google.de/maps/search/**
// @match        *://*.google.dj/maps/search/**
// @match        *://*.google.dk/maps/search/**
// @match        *://*.google.dm/maps/search/**
// @match        *://*.google.com.do/maps/search/**
// @match        *://*.google.dz/maps/search/**
// @match        *://*.google.com.ec/maps/search/**
// @match        *://*.google.ee/maps/search/**
// @match        *://*.google.com.eg/maps/search/**
// @match        *://*.google.es/maps/search/**
// @match        *://*.google.com.et/maps/search/**
// @match        *://*.google.fi/maps/search/**
// @match        *://*.google.com.fj/maps/search/**
// @match        *://*.google.fm/maps/search/**
// @match        *://*.google.fr/maps/search/**
// @match        *://*.google.ga/maps/search/**
// @match        *://*.google.ge/maps/search/**
// @match        *://*.google.gf/maps/search/**
// @match        *://*.google.gg/maps/search/**
// @match        *://*.google.com.gh/maps/search/**
// @match        *://*.google.com.gi/maps/search/**
// @match        *://*.google.gl/maps/search/**
// @match        *://*.google.gm/maps/search/**
// @match        *://*.google.gp/maps/search/**
// @match        *://*.google.gr/maps/search/**
// @match        *://*.google.com.gt/maps/search/**
// @match        *://*.google.gy/maps/search/**
// @match        *://*.google.com.hk/maps/search/**
// @match        *://*.google.hn/maps/search/**
// @match        *://*.google.hr/maps/search/**
// @match        *://*.google.ht/maps/search/**
// @match        *://*.google.hu/maps/search/**
// @match        *://*.google.co.id/maps/search/**
// @match        *://*.google.iq/maps/search/**
// @match        *://*.google.ie/maps/search/**
// @match        *://*.google.co.il/maps/search/**
// @match        *://*.google.im/maps/search/**
// @match        *://*.google.co.in/maps/search/**
// @match        *://*.google.io/maps/search/**
// @match        *://*.google.is/maps/search/**
// @match        *://*.google.it/maps/search/**
// @match        *://*.google.je/maps/search/**
// @match        *://*.google.com.jm/maps/search/**
// @match        *://*.google.jo/maps/search/**
// @match        *://*.google.co.jp/maps/search/**
// @match        *://*.google.co.ke/maps/search/**
// @match        *://*.google.ki/maps/search/**
// @match        *://*.google.kg/maps/search/**
// @match        *://*.google.co.kr/maps/search/**
// @match        *://*.google.com.kw/maps/search/**
// @match        *://*.google.kz/maps/search/**
// @match        *://*.google.la/maps/search/**
// @match        *://*.google.com.lb/maps/search/**
// @match        *://*.google.com.lc/maps/search/**
// @match        *://*.google.li/maps/search/**
// @match        *://*.google.lk/maps/search/**
// @match        *://*.google.co.ls/maps/search/**
// @match        *://*.google.lt/maps/search/**
// @match        *://*.google.lu/maps/search/**
// @match        *://*.google.lv/maps/search/**
// @match        *://*.google.com.ly/maps/search/**
// @match        *://*.google.co.ma/maps/search/**
// @match        *://*.google.md/maps/search/**
// @match        *://*.google.me/maps/search/**
// @match        *://*.google.mg/maps/search/**
// @match        *://*.google.mk/maps/search/**
// @match        *://*.google.ml/maps/search/**
// @match        *://*.google.com.mm/maps/search/**
// @match        *://*.google.mn/maps/search/**
// @match        *://*.google.ms/maps/search/**
// @match        *://*.google.com.mt/maps/search/**
// @match        *://*.google.mu/maps/search/**
// @match        *://*.google.mv/maps/search/**
// @match        *://*.google.mw/maps/search/**
// @match        *://*.google.com.mx/maps/search/**
// @match        *://*.google.com.my/maps/search/**
// @match        *://*.google.co.mz/maps/search/**
// @match        *://*.google.com.na/maps/search/**
// @match        *://*.google.ne/maps/search/**
// @match        *://*.google.com.nf/maps/search/**
// @match        *://*.google.com.ng/maps/search/**
// @match        *://*.google.com.ni/maps/search/**
// @match        *://*.google.nl/maps/search/**
// @match        *://*.google.no/maps/search/**
// @match        *://*.google.com.np/maps/search/**
// @match        *://*.google.nr/maps/search/**
// @match        *://*.google.nu/maps/search/**
// @match        *://*.google.co.nz/maps/search/**
// @match        *://*.google.com.om/maps/search/**
// @match        *://*.google.com.pk/maps/search/**
// @match        *://*.google.com.pa/maps/search/**
// @match        *://*.google.com.pe/maps/search/**
// @match        *://*.google.com.ph/maps/search/**
// @match        *://*.google.pl/maps/search/**
// @match        *://*.google.com.pg/maps/search/**
// @match        *://*.google.pn/maps/search/**
// @match        *://*.google.com.pr/maps/search/**
// @match        *://*.google.ps/maps/search/**
// @match        *://*.google.pt/maps/search/**
// @match        *://*.google.com.py/maps/search/**
// @match        *://*.google.com.qa/maps/search/**
// @match        *://*.google.ro/maps/search/**
// @match        *://*.google.rs/maps/search/**
// @match        *://*.google.ru/maps/search/**
// @match        *://*.google.rw/maps/search/**
// @match        *://*.google.com.sa/maps/search/**
// @match        *://*.google.com.sb/maps/search/**
// @match        *://*.google.sc/maps/search/**
// @match        *://*.google.co.th/maps/search/**
// @match        *://*.google.com.tj/maps/search/**
// @match        *://*.google.tk/maps/search/**
// @match        *://*.google.tl/maps/search/**
// @match        *://*.google.tm/maps/search/**
// @match        *://*.google.to/maps/search/**
// @match        *://*.google.tn/maps/search/**
// @match        *://*.google.com.tr/maps/search/**
// @match        *://*.google.tt/maps/search/**
// @match        *://*.google.com.tw/maps/search/**
// @match        *://*.google.co.tz/maps/search/**
// @match        *://*.google.se/maps/search/**
// @match        *://*.google.com.sg/maps/search/**
// @match        *://*.google.sh/maps/search/**
// @match        *://*.google.si/maps/search/**
// @match        *://*.google.sk/maps/search/**
// @match        *://*.google.com.sl/maps/search/**
// @match        *://*.google.sn/maps/search/**
// @match        *://*.google.sm/maps/search/**
// @match        *://*.google.so/maps/search/**
// @match        *://*.google.st/maps/search/**
// @match        *://*.google.sr/maps/search/**
// @match        *://*.google.com.sv/maps/search/**
// @match        *://*.google.td/maps/search/**
// @match        *://*.google.tg/maps/search/**
// @match        *://*.google.com.ua/maps/search/**
// @match        *://*.google.co.ug/maps/search/**
// @match        *://*.google.co.uk/maps/search/**
// @match        *://*.google.com/maps/search/**
// @match        *://*.google.com.uy/maps/search/**
// @match        *://*.google.co.uz/maps/search/**
// @match        *://*.google.com.vc/maps/search/**
// @match        *://*.google.co.ve/maps/search/**
// @match        *://*.google.vg/maps/search/**
// @match        *://*.google.co.vi/maps/search/**
// @match        *://*.google.com.vn/maps/search/**
// @match        *://*.google.vu/maps/search/**
// @match        *://*.google.ws/maps/search/**
// @match        *://*.google.co.za/maps/search/**
// @match        *://*.google.co.zm/maps/search/**
// @match        *://*.google.co.zw/maps/search/**
// @supportURL   https://github.com/webAutomationLover/google-map-scraper/issues
// @homepageURL  https://github.com/webAutomationLover/google-map-scraper
// @icon         https://www.google.com/s2/favicons?sz=64&domain=xiaohongshu.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @resource     bootstrapCSS https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @require      https://code.jquery.com/jquery-3.5.1.min.js
// @require      https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    class DataManager {
        constructor() {
            this.data = new Map();
            this.errorLog = [];
            this.defaultFields = new Set([
                'name', 'fullAddress', 'phones', 'website',
                'averageRating', 'reviewCount', 'categories', 'emails', 'plusCode'
            ]);
            this.selectedFields = new Set(this.defaultFields);
            this.loadSelectedFields();
            this.validationRules = {
                name: { required: true, minLength: 1 },
                fullAddress: { required: true, minLength: 5 },
                phones: { pattern: /^[0-9+\s-]+$/ },
                website: { pattern: /^https?:\/\/.+/ },
                averageRating: { min: 0, max: 5 },
                reviewCount: { min: 0 },
                emails: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
                plusCode: { pattern: /^[23456789CFGHJMPQRVWX]{8}\+[23456789CFGHJMPQRVWX]{2}$/ }
            };
        }

        loadSelectedFields() {
            const savedFields = localStorage.getItem('googleMapsScraperSelectedFields');
            if (savedFields) {
                this.selectedFields = new Set(JSON.parse(savedFields));
            }
        }

        saveSelectedFields() {
            localStorage.setItem('googleMapsScraperSelectedFields',
                JSON.stringify(Array.from(this.selectedFields)));
        }

        addItem(item) {
            if (!item || !item.placeId) {
                console.log('Skipping item: Missing placeId');
                return;
            }

            const isDuplicate = this.data.has(item.placeId);
            if (isDuplicate) {
                console.log(`Duplicate record found - placeId: ${item.placeId}, name: ${item.name}`);
            } else {
                console.log(`New record added - placeId: ${item.placeId}, name: ${item.name}`);
            }

            this.data.set(item.placeId, item);
        }

        getItems() {
            return Array.from(this.data.values());
        }

        getCount() {
            return this.data.size;
        }

        logError(error) {
            this.errorLog.push({
                timestamp: new Date().toISOString(),
                error: error.message || error
            });
            console.error('Error:', error);
        }

        getErrors() {
            return this.errorLog;
        }

        getSelectedFields() {
            return Array.from(this.selectedFields);
        }

        setSelectedFields(fields) {
            this.selectedFields = new Set(fields);
            this.saveSelectedFields();
        }

        getPreviewData(limit = 5) {
            return this.getItems().slice(0, limit);
        }

        validateItem(item) {
            const errors = [];
            Object.entries(this.validationRules).forEach(([field, rules]) => {
                const value = item[field];

                if (rules.required && (!value || value.length === 0)) {
                    errors.push(`${field} is required`);
                }

                if (value) {
                    if (rules.minLength && value.length < rules.minLength) {
                        errors.push(`${field} length cannot be less than ${rules.minLength}`);
                    }
                    if (rules.pattern && !rules.pattern.test(value)) {
                        errors.push(`${field} format is incorrect`);
                    }
                    if (rules.min !== undefined && value < rules.min) {
                        errors.push(`${field} cannot be less than ${rules.min}`);
                    }
                    if (rules.max !== undefined && value > rules.max) {
                        errors.push(`${field} cannot be greater than ${rules.max}`);
                    }
                }
            });
            return errors;
        }

        cleanData() {
            const cleanedData = new Map();
            this.data.forEach((item, key) => {
                const cleanedItem = { ...item };
                // Clean phone number format
                if (cleanedItem.phones) {
                    cleanedItem.phones = cleanedItem.phones.replace(/[^\d+]/g, '');
                }
                // Clean website format
                if (cleanedItem.website && !cleanedItem.website.startsWith('http')) {
                    cleanedItem.website = 'https://' + cleanedItem.website;
                }
                // Clean rating
                if (cleanedItem.averageRating) {
                    cleanedItem.averageRating = Math.min(5, Math.max(0, parseFloat(cleanedItem.averageRating)));
                }
                cleanedData.set(key, cleanedItem);
            });
            this.data = cleanedData;
        }

        prepareData() {
            this.cleanData();
            const data = this.getItems().map(item => {
                const filteredItem = {};
                this.getSelectedFields().forEach(field => {
                    filteredItem[field] = item[field];
                });
                return filteredItem;
            });

            const validationErrors = [];
            data.forEach((item, index) => {
                const errors = this.validateItem(item);
                if (errors.length > 0) {
                    validationErrors.push({
                        index,
                        item: item.name || `Item ${index}`,
                        errors
                    });
                }
            });

            if (validationErrors.length > 0) {
                console.warn('Data validation warnings:', validationErrors);
                this.logError({
                    type: 'validation',
                    errors: validationErrors
                });
            }

            return data;
        }

        resetToDefaultFields() {
            this.selectedFields = new Set(this.defaultFields);
            this.saveSelectedFields();
        }
    }

    class ConfigManager {
        constructor() {
            this.config = {
                scrollSpeed: 1000,
                scrollInterval: {
                    min: 5,
                    max: 10
                },
                emailExtraction: {
                    enabled: false,
                    apiUrl: "https://g2.converts.workers.dev/"
                },
                plusCodeExtraction: {
                    enabled: false,
                    apiUrl: "https://plus.codes/api"
                }
            };
            this.loadConfig();
            this.onConfigChange = null;
        }

        loadConfig() {
            const savedConfig = localStorage.getItem('googleMapsScraperConfig');
            if (savedConfig) {
                this.config = { ...this.config, ...JSON.parse(savedConfig) };
            }
        }

        saveConfig() {
            localStorage.setItem('googleMapsScraperConfig', JSON.stringify(this.config));
        }

        updateConfig(newConfig) {
            this.config = { ...this.config, ...newConfig };
            this.saveConfig();
            if (this.onConfigChange) {
                this.onConfigChange(this.config);
            }
        }
    }

    class AutoScrollManager {
        constructor() {
            this.scrollTimer = null;
            this.countdownTimer = null;
            this.isLoading = false;
            this.createCountdownDisplay();
        }

        createCountdownDisplay() {
            this.countdownDisplay = $('<div class="countdown-display" style="display: none; position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 9999;"></div>');
            $('body').append(this.countdownDisplay);
        }

        startCountdown(seconds) {
            if (this.countdownTimer) {
                clearInterval(this.countdownTimer);
            }

            this.countdownDisplay.show();
            let remainingSeconds = seconds;

            const updateDisplay = () => {
                this.countdownDisplay.text(`Next scroll in ${remainingSeconds}s`);
                if (remainingSeconds <= 0) {
                    clearInterval(this.countdownTimer);
                    this.countdownTimer = null;
                }
                remainingSeconds--;
            };

            updateDisplay();
            this.countdownTimer = setInterval(updateDisplay, 1000);
        }

        getRandomInteger(min, max) {
            return Math.floor(Math.random() * (max - min + 1)) + min;
        }

        checkIfReachedEnd() {
            const elScroll = document.querySelector('[role="feed"]');
            if (!elScroll) return false;

            const lastChild = elScroll.lastElementChild;
            return lastChild && lastChild.getAttribute('style')?.includes('height: 64px');
        }

        scroll() {
            const elScroll = document.querySelector('[role="feed"]');
            if (!elScroll) {
                console.warn("Scrollable element not found. Stopping auto-scroll.");
                this.stop();
                return;
            }

            // Read the latest config before each scroll
            const config = configManager.config;
            elScroll.scrollBy({
                top: config.scrollSpeed,
                behavior: 'smooth'
            });

            if (this.checkIfReachedEnd()) {
                console.log("Reached end of results.");
                this.stop();
                return;
            }

            // Use the latest config to calculate the next scroll interval
            const nextInterval = this.getRandomInteger(
                config.scrollInterval.min,
                config.scrollInterval.max
            );
            this.startCountdown(nextInterval);
            this.scrollTimer = setTimeout(() => this.scroll(), nextInterval * 1000);
        }

        start() {
            if (this.isLoading) return;

            // Check if already at the end
            if (this.checkIfReachedEnd()) {
                alert('Already reached the end of results. Please try a new search or move the map to load more results.');
                return;
            }

            this.isLoading = true;
            console.log('Starting auto scroll with config:', configManager.config);

            // Update button states
            startAutoScrollButton.html('Stop Auto Scroll');
            startAutoScrollButton.removeClass('btn-secondary').addClass('btn-danger');
            bsButton.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Export Data (' + dataManager.getCount() + ')');

            // Use the latest config to start the first scroll
            const config = configManager.config;
            const firstInterval = this.getRandomInteger(
                config.scrollInterval.min,
                config.scrollInterval.max
            );
            this.startCountdown(firstInterval);
            this.scrollTimer = setTimeout(() => this.scroll(), firstInterval * 1000);
        }

        stop() {
            if (this.scrollTimer) {
                clearTimeout(this.scrollTimer);
                this.scrollTimer = null;
            }
            if (this.countdownTimer) {
                clearInterval(this.countdownTimer);
                this.countdownTimer = null;
            }
            this.isLoading = false;
            this.countdownDisplay.hide();

            // Reset button states
            startAutoScrollButton.html('Start Auto Scroll');
            startAutoScrollButton.removeClass('btn-danger').addClass('btn-secondary');
            bsButton.prop('disabled', false).html('Export Data (' + dataManager.getCount() + ')');
        }
    }

    class ExportManager {
        constructor(dataManager) {
            this.dataManager = dataManager;
        }

        getFileName(extension) {
            const now = new Date();
            const timestamp = now.getFullYear() + '-' +
                String(now.getMonth() + 1).padStart(2, '0') + '-' +
                String(now.getDate()).padStart(2, '0') + '-' +
                String(now.getHours()).padStart(2, '0') + ':' +
                String(now.getMinutes()).padStart(2, '0') + ':' +
                String(now.getSeconds()).padStart(2, '0');
            return `google_maps_data_${timestamp}.${extension}`;
        }

        exportToXLSX() {
            try {
                const data = this.dataManager.prepareData();
                const ws = XLSX.utils.json_to_sheet(data);
                const wb = XLSX.utils.book_new();
                XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');

                if (this.dataManager.getErrors().length > 0) {
                    const errorWs = XLSX.utils.json_to_sheet(this.dataManager.getErrors());
                    XLSX.utils.book_append_sheet(wb, errorWs, 'Errors');
                }

                // Convert to binary string
                const wbout = XLSX.write(wb, {
                    bookType: 'xlsx',
                    type: 'binary',
                    bookSST: true
                });

                // Convert binary string to array buffer
                const buf = new ArrayBuffer(wbout.length);
                const view = new Uint8Array(buf);
                for (let i = 0; i < wbout.length; i++) {
                    view[i] = wbout.charCodeAt(i) & 0xFF;
                }

                // Create blob and download
                const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
                this.downloadFile(blob, this.getFileName('xlsx'), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to XLSX. Please check console for details.');
            }
        }

        exportToCSV() {
            try {
                const data = this.dataManager.prepareData();
                const ws = XLSX.utils.json_to_sheet(data);
                const csv = XLSX.utils.sheet_to_csv(ws);
                this.downloadFile(csv, this.getFileName('csv'), 'text/csv');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to CSV. Please check console for details.');
            }
        }

        exportToJSON() {
            try {
                const data = this.dataManager.prepareData();
                const json = JSON.stringify(data, null, 2);
                this.downloadFile(json, this.getFileName('json'), 'application/json');
            } catch (error) {
                this.dataManager.logError(error);
                alert('Error exporting to JSON. Please check console for details.');
            }
        }

        downloadFile(content, filename, mimeType) {
            const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = filename;
            a.click();
            URL.revokeObjectURL(a.href);
        }
    }

    const dataManager = new DataManager();
    const configManager = new ConfigManager();
    const autoScrollManager = new AutoScrollManager();
    const exportManager = new ExportManager(dataManager);

    window.jsonArr = [];

    GM_addStyle(GM_getResourceText("bootstrapCSS"));

    const modalHTML = `
        <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="myModalLabel">Bootstrap Modal</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">×</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        This is the content of the Bootstrap modal.
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary">Save changes</button>
                    </div>
                </div>
            </div>
        </div>
    `;
    $('body').append(modalHTML);

    const bsButton = $('<button type="button" class="btn btn-primary">Export Data (0)</button>');
    const startAutoScrollButton = $('<button type="button" class="btn btn-secondary ml-2" id="start-scroll-button">Start Auto Scroll</button>');
    const configButton = $('<button type="button" class="btn btn-outline-secondary ml-2" id="config-button"><i class="fas fa-cog"></i></button>');

    bsButton.click(function() {
        updateFieldSelector();
        updatePreviewTable();
        $('#previewModal').modal('show');
    });

    function updateButtonText() {
        bsButton.text('Export Data (' + dataManager.getCount() + ')');
    }

    startAutoScrollButton.click(() => {
        console.log('Start auto scroll button clicked');
        if (autoScrollManager.isLoading) {
            autoScrollManager.stop();
        } else {
            autoScrollManager.start();
        }
    });

    // Create configuration panel
    const configModalHTML = `
        <div class="modal fade" id="configModal" tabindex="-1" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Settings</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <form id="configForm">
                            <div class="form-group">
                                <label>Scroll Speed (pixels/second)</label>
                                <input type="number" class="form-control" id="scrollSpeed" 
                                       value="${configManager.config.scrollSpeed}">
                            </div>
                            <div class="form-group">
                                <label>Scroll Interval (seconds)</label>
                                <small class="form-text text-muted mb-2">
                                    Set a range for random interval between scrolls. The script will randomly choose a value between min and max.
                                </small>
                                <div class="row">
                                    <div class="col">
                                        <input type="number" class="form-control" id="scrollIntervalMin" 
                                               placeholder="Min" value="${configManager.config.scrollInterval.min}">
                                    </div>
                                    <div class="col">
                                        <input type="number" class="form-control" id="scrollIntervalMax" 
                                               placeholder="Max" value="${configManager.config.scrollInterval.max}">
                                    </div>
                                </div>
                            </div>
                            <div class="form-group">
                                <div class="custom-control custom-switch">
                                    <input type="checkbox" class="custom-control-input" id="emailExtractionEnabled" 
                                           ${configManager.config.emailExtraction.enabled ? 'checked' : ''}>
                                    <label class="custom-control-label" for="emailExtractionEnabled">Enable Email Extraction</label>
                                </div>
                                <small class="form-text text-muted">
                                    When enabled, the script will attempt to extract email addresses from business websites.
                                </small>
                            </div>
                            <div class="form-group">
                                <div class="custom-control custom-switch">
                                    <input type="checkbox" class="custom-control-input" id="plusCodeExtractionEnabled" 
                                           ${configManager.config.plusCodeExtraction.enabled ? 'checked' : ''}>
                                    <label class="custom-control-label" for="plusCodeExtractionEnabled">Enable Plus Code Extraction</label>
                                </div>
                                <small class="form-text text-muted">
                                    Plus Codes are a type of digital address system developed by Google to provide precise location information. They are especially useful in areas where traditional street addresses are not available or are difficult to use.
                                </small>
                            </div>
                        </form>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary" id="saveConfig">Save Settings</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    $('body').append(configModalHTML);

    // Add configuration button
    configButton.click(() => {
        console.log('Config button clicked');
        try {
            if ($('#configModal').length === 0) {
                console.error('Config modal not found in DOM');
                return;
            }
            $('#configModal').modal('show');
        } catch (error) {
            console.error('Error showing config modal:', error);
            alert('Unable to open settings panel. Please refresh the page and try again.');
        }
    });

    // Save configuration
    $('#saveConfig').click(() => {
        const newConfig = {
            scrollSpeed: parseInt($('#scrollSpeed').val()),
            scrollInterval: {
                min: parseInt($('#scrollIntervalMin').val()),
                max: parseInt($('#scrollIntervalMax').val())
            },
            emailExtraction: {
                enabled: $('#emailExtractionEnabled').is(':checked'),
                apiUrl: configManager.config.emailExtraction.apiUrl
            },
            plusCodeExtraction: {
                enabled: $('#plusCodeExtractionEnabled').is(':checked'),
                apiUrl: configManager.config.plusCodeExtraction.apiUrl
            }
        };

        configManager.updateConfig(newConfig);

        $('#configModal').modal('hide');
    });

    // XPath helper function
    function getElementByXPath(xpath) {
        return document.evaluate(
            xpath,
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
    }

    // Modify button injection function
    const injectButton = () => {
        // Use XPath to find the target div (button's grandparent)
        const targetDiv = getElementByXPath("//button[contains(@class, 'e2moi') and not(contains(@jsaction, 'ripple')) and @aria-haspopup='menu']/../..");
        
        if (targetDiv && !document.querySelector('#my-custom-button')) {
            console.log('Injecting buttons via XPath');
            const bsButtonDOM = bsButton[0];
            bsButtonDOM.id = 'my-custom-button';
            bsButtonDOM.style.marginLeft = '20px';

            const parentDiv = targetDiv;
            parentDiv.appendChild(bsButtonDOM);
            parentDiv.appendChild(startAutoScrollButton[0]);
            parentDiv.appendChild(configButton[0]);
        }
    };

    // Add debug logs
    const observer = new MutationObserver(mutations => {
        console.log('DOM mutation detected');
        injectButton();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
    });

    // Modify XHR to capture data
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        // Check if this is the target URL
        const isTargetUrl = this._url && this._url.includes('/search') && this._url.includes('tbm=map');
        
        if (isTargetUrl) {
            console.log('🎯 [TARGET] send api:', this._url);
            console.log('  - onload exists?', this.onload !== null && this.onload !== undefined);
            console.log('  - onreadystatechange exists?', this.onreadystatechange !== null && this.onreadystatechange !== undefined);
        }

        if (!this._listenerAdded) {
            this._listenerAdded = true;
            
            // readystatechange is more low-level and reliable
            this.addEventListener('readystatechange', function() {
                const isTarget = this._url && this._url.includes('/search') && this._url.includes('tbm=map');
                
                // Only log status for target URL
                if (isTarget) {
                    console.log('📡 [TARGET readyState=' + this.readyState + '] URL:', this._url);
                    
                    if (this.readyState === 4) {
                        console.log('📡 [TARGET State 4 - DONE]');
                        console.log('  - Status:', this.status);
                        console.log('  - StatusText:', this.statusText);
                        console.log('  - Response exists:', !!this.responseText);
                        console.log('  - Response length:', this.responseText ? this.responseText.length : 0);
                        console.log('  - URL check: includes(/search)=', this._url.includes('/search'));
                        console.log('  - URL check: includes(tbm=map)=', this._url.includes('tbm=map'));
                        
                        if (this.status === 200) {
                            console.log('✅ [TARGET] Status is 200, should capture!');
                            console.log('Response preview:', this.responseText ? this.responseText.substring(0, 100) : 'empty');
                        } else {
                            console.log('❌ [TARGET] Status is NOT 200, cannot capture');
                        }
                    }
                }
            });
        }

        this.addEventListener('load', function() {
            const isTarget = this._url && this._url.includes('/search') && this._url.includes('tbm=map');
            
            if (isTarget) {
                console.log('🎉 [TARGET] LOAD EVENT FIRED!');
            }
            
            if (this._url.includes('/search?tbm=map')) {
                try {
                    var rspJson = JSON.parse(this.responseText.replace(`/*""*/`,""));
                    var e = rspJson.d;
                    var cleanedData = e.replace(`)]}'`, "");

                    let parsedData = JSON.parse(cleanedData);
                    let dataList = parsedData[0][1];

                    let filteredData = dataList.filter(item => {
                        return item?.[14] !== undefined;
                    });

                    if (!filteredData || filteredData.length < 1) {
                        filteredData = parsedData[64];
                    }

                    if (filteredData) {
                        var formatedData = formatAllData(filteredData);
                        formatedData.forEach(item => dataManager.addItem(item));
                        console.log('Total items:', dataManager.getCount());
                        // Update export button with loading icon if auto-scroll is active
                        if (autoScrollManager.isLoading) {
                            bsButton.html('<i class="fas fa-spinner fa-spin"></i> Export Data (' + dataManager.getCount() + ')');
                        } else {
                            updateButtonText();
                        }
                    }
                } catch (error) {
                    dataManager.logError(error);
                }
            }
        });
        
        // Only monitor error and abort events for target URL
        if (isTargetUrl) {
            this.addEventListener('error', function() {
                console.log('❌ [TARGET] ERROR EVENT - Request failed!');
            });
            
            this.addEventListener('abort', function() {
                console.log('⚠️⚠️⚠️ [TARGET] ABORT EVENT - Request was cancelled!');
                console.log('  - This is why we never reach State 4');
                console.log('  - Google Maps is cancelling the request');
            });
            
            this.addEventListener('timeout', function() {
                console.log('⏱️ [TARGET] TIMEOUT EVENT');
            });
            
            // Hook the abort method
            const originalAbort = this.abort;
            const xhrInstance = this;
            
            this.abort = function() {
                console.log('🛑🛑🛑 [TARGET] abort() is being called!');
                console.log('  - Current readyState:', xhrInstance.readyState);
                console.log('  - Current status:', xhrInstance.status);
                console.log('  - Response available:', !!xhrInstance.responseText);
                console.log('  - Response length:', xhrInstance.responseText ? xhrInstance.responseText.length : 0);
                
                // If response data is available, try to process it before abort
                if (xhrInstance.responseText && xhrInstance.responseText.length > 0) {
                    console.log('💾 [TARGET] Trying to save data BEFORE abort...');
                    try {
                        var rspJson = JSON.parse(xhrInstance.responseText.replace(`/*""*/`,""));
                        var e = rspJson.d;
                        var cleanedData = e.replace(`)]}'`, "");
                        let parsedData = JSON.parse(cleanedData);
                        let dataList = parsedData[0][1];
                        
                        let filteredData = dataList.filter(item => {
                            return item?.[14] !== undefined;
                        });
                        
                        if (!filteredData || filteredData.length < 1) {
                            filteredData = parsedData[64];
                        }
                        
                        if (filteredData) {
                            var formatedData = formatAllData(filteredData);
                            formatedData.forEach(item => dataManager.addItem(item));
                            console.log('✅ [TARGET] Data saved before abort! Total items:', dataManager.getCount());
                            updateButtonText();
                        }
                    } catch (error) {
                        console.log('❌ [TARGET] Failed to parse data before abort:', error.message);
                    }
                } else {
                    console.log('⚠️ [TARGET] No response data available before abort');
                }
                
                console.trace('Abort call stack:');
                return originalAbort.apply(this, arguments);
            };
        }
        
        return originalSend.apply(this, arguments);
    };

    // Format data for export
    function formatAllData(allDataList) {
        return allDataList.map(d => formatDataItem(d)).filter(d => d.name);
    }

    // Format individual data item
    function formatDataItem(item) {
        const fieldConfig = {
            fullAddress: [39],
            placeId: [78],
            kgmid: [89],
            categories: [13],
            feature: [32, 0, 1],
            cid: [10],
            featuredImage: [37, 0, 0, 6, 0],
            phones: [],
            icon: [122, 0, 1],
            name: [11],
            latitude: [9, 2],
            longitude: [9, 3],
            reviewCount: [4, 8],
            reviewURL: [4, 3, 0],
            averageRating: [4, 7],
            street: [183, 0, 0, 1, 1],
            municipality: [183, 1, 3],
            openingHours: [],
            website: [7, 0],
            domain: [7, 1],
            emails: [],
            plusCode: []
        };

        const resultData = {};
        Object.keys(fieldConfig).forEach(key => {
            resultData[key] = handleSingleField(fieldConfig[key]);
        });

        // Process special fields
        resultData.phones = handleSingleField([178, 0, 1])?.map(d => d?.[0]);
        resultData.openingHours = handleSingleField([34, 1])?.map(d => [`${d[0]}:[${d[1]}]`])?.join(', ');
        resultData.googleMapsURL = "https://www.google.com/maps?cid=".concat(resultData.cid);
        resultData.googleKnowledgeURL = "https://www.google.com/maps/search/*?kgmid=".concat(resultData.kgmid, "&kponly");

        // Format array fields
        resultData.phones = resultData.phones?.join?.(', ');
        resultData.categories = resultData.categories?.join?.(', ');
        resultData.street = resultData.street?.join?.(', ');
        resultData.emails = '';
        resultData.plusCode = '';

        // Fetch emails if website exists and email extraction is enabled
        if (resultData.website && configManager.config.emailExtraction.enabled) {
            fetchEmails(resultData.website).then(emails => {
                resultData.emails = emails.join(', ');
                updateDataItem(resultData);
            });
        }

        // Fetch plus code if coordinates exist and plus code extraction is enabled
        if (resultData.latitude && resultData.longitude && configManager.config.plusCodeExtraction.enabled) {
            fetchPlusCode(resultData.latitude, resultData.longitude).then(plusCode => {
                resultData.plusCode = plusCode;
                updateDataItem(resultData);
            });
        }

        function updateDataItem(data) {
            if (data.placeId) {
                const existingItem = dataManager.data.get(data.placeId);
                if (existingItem) {
                    Object.assign(existingItem, data);
                    dataManager.data.set(data.placeId, existingItem);
                    updatePreviewTable();
                }
            }
        }

        function handleSingleField(config) {
            const itemData = item[1];
            if (!itemData) {
                return;
            }
            if (!config || !config.length) {
                return;
            }
            let currentData = itemData;
            for (let i = 0; i < config.length; i++) {
                currentData = currentData?.[config[i]];
            }
            return currentData;
        }

        return resultData;
    }

    // Create preview modal
    const previewModalHTML = `
        <div class="modal fade" id="previewModal" tabindex="-1" role="dialog">
            <div class="modal-dialog modal-lg" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Data Preview</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <label class="mb-0">Select Export Fields:</label>
                                <div>
                                    <button type="button" class="btn btn-sm btn-outline-secondary mr-2" id="selectAllFields">Select All</button>
                                    <button type="button" class="btn btn-sm btn-outline-secondary" id="resetFields">Reset</button>
                                </div>
                            </div>
                            <div id="fieldSelector" class="d-flex flex-wrap">
                            </div>
                        </div>
                        <div class="table-responsive">
                            <table class="table table-striped">
                                <thead id="previewTableHead"></thead>
                                <tbody id="previewTableBody"></tbody>
                            </table>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-danger" id="clearData">Clear Data</button>
                        <button type="button" class="btn btn-success" id="exportXLSX">Export XLSX</button>
                        <button type="button" class="btn btn-info" id="exportCSV">Export CSV</button>
                        <button type="button" class="btn btn-warning" id="exportJSON">Export JSON</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    // Create confirmation modal for clearing data
    const confirmModalHTML = `
        <div class="modal fade" id="confirmClearModal" tabindex="-1" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Confirm Clear Data</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>Are you sure you want to clear all collected data? This action cannot be undone.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                        <button type="button" class="btn btn-danger" id="confirmClear">Clear All Data</button>
                    </div>
                </div>
            </div>
        </div>
    `;

    $('body').append(previewModalHTML);
    $('body').append(confirmModalHTML);

    function updatePreviewTable() {
        const previewData = dataManager.getPreviewData();
        const fields = dataManager.getSelectedFields();

        // Update table header
        const thead = $('#previewTableHead');
        thead.empty();
        const headerRow = $('<tr>');
        fields.forEach(field => {
            headerRow.append(`<th>${field}</th>`);
        });
        thead.append(headerRow);

        // Update table content
        const tbody = $('#previewTableBody');
        tbody.empty();
        previewData.forEach(item => {
            const row = $('<tr>');
            fields.forEach(field => {
                row.append(`<td>${item[field] || ''}</td>`);
            });
            tbody.append(row);
        });
    }

    function updateFieldSelector() {
        const fieldSelector = $('#fieldSelector');
        fieldSelector.empty();

        const allFields = [
            'name', 'fullAddress', 'phones', 'website', 'averageRating',
            'reviewCount', 'categories', 'emails', 'featuredImage', 'latitude',
            'longitude', 'street', 'municipality', 'openingHours',
            'placeId', 'kgmid', 'feature', 'cid', 'icon', 'reviewURL',
            'domain', 'googleMapsURL', 'googleKnowledgeURL', 'plusCode'
        ];

        allFields.forEach(field => {
            const div = $(`
                <div class="custom-control custom-checkbox mr-3 mb-2">
                    <input type="checkbox" class="custom-control-input" id="field_${field}" 
                           ${dataManager.selectedFields.has(field) ? 'checked' : ''}>
                    <label class="custom-control-label" for="field_${field}">${field}</label>
                </div>
            `);
            fieldSelector.append(div);
        });

        // Add field selection event listener
        $('.custom-control-input').change(function() {
            const field = $(this).attr('id').replace('field_', '');
            if (this.checked) {
                dataManager.selectedFields.add(field);
                dataManager.saveSelectedFields();
                updatePreviewTable();
            } else {
                // Check if this is the last selected field
                if (dataManager.selectedFields.size <= 1) {
                    // Prevent unchecking the last field
                    $(this).prop('checked', true);
                    alert('At least one export field must be selected');
                    return;
                }
                dataManager.selectedFields.delete(field);
                dataManager.saveSelectedFields();
                updatePreviewTable();
            }
        });

        // Add select all button event listener
        $('#selectAllFields').off('click').on('click', () => {
            $('.custom-control-input').prop('checked', true);
            allFields.forEach(field => {
                dataManager.selectedFields.add(field);
            });
            dataManager.saveSelectedFields();
            updatePreviewTable();
        });

        // Add reset button event listener
        $('#resetFields').off('click').on('click', () => {
            dataManager.resetToDefaultFields();
            // Update checkboxes to match default fields
            $('.custom-control-input').each(function() {
                const field = $(this).attr('id').replace('field_', '');
                $(this).prop('checked', dataManager.selectedFields.has(field));
            });
            updatePreviewTable();
        });
    }

    // Add export button event listener
    $('#exportXLSX').click(() => {
        exportManager.exportToXLSX();
        $('#previewModal').modal('hide');
    });

    $('#exportCSV').click(() => {
        exportManager.exportToCSV();
        $('#previewModal').modal('hide');
    });

    $('#exportJSON').click(() => {
        exportManager.exportToJSON();
        $('#previewModal').modal('hide');
    });

    // Add clear data button event listener
    $('#clearData').click(() => {
        $('#confirmClearModal').modal('show');
    });

    // Add confirm clear button event listener
    $('#confirmClear').click(() => {
        dataManager.data.clear();
        dataManager.errorLog = [];
        updateButtonText();
        updatePreviewTable();
        $('#confirmClearModal').modal('hide');
        $('#previewModal').modal('hide');
    });

    // Add Font Awesome for icons
    const fontAwesomeLink = $('<link>', {
        rel: 'stylesheet',
        href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'
    });
    $('head').append(fontAwesomeLink);

    async function fetchEmails(url) {
        if (!configManager.config.emailExtraction.enabled) {
            return [];
        }

        try {
            const apiUrl = "https://g2.converts.workers.dev/".concat(url);
            const response = await fetch(apiUrl);

            if (response.status !== 200) {
                console.log('Email extraction failed for URL:', url);
                return [];
            }

            const data = await response.json();
            if (data && data.emails && data.emails.length > 0) {
                console.log('URL:', url);
                console.log('Extracted emails:', data.emails);
                return data.emails;
            }

            return [];
        } catch (error) {
            console.error('Error fetching emails for URL:', url, error);
            return [];
        }
    }

    async function fetchPlusCode(lat, long) {
        if (!configManager.config.plusCodeExtraction.enabled) {
            return "";
        }

        try {
            const apiUrl = `${configManager.config.plusCodeExtraction.apiUrl}?address=${lat},${long}&format=json`;
            const response = await fetch(apiUrl);

            if (response.status !== 200) {
                console.log('Plus code extraction failed for coordinates:', lat, long);
                return "";
            }

            const data = await response.json();
            if (data && data.plus_code && data.plus_code.global_code) {
                console.log('Coordinates:', lat, long);
                console.log('Extracted plus code:', data.plus_code.global_code);
                return data.plus_code.global_code;
            }

            return "";
        } catch (error) {
            console.error('Error fetching plus code for coordinates:', lat, long, error);
            return "";
        }
    }

})();