WME Places Name Normalizer

Normaliza nombres de lugares y gestiona categorías dinámicamente en WME.

当前为 2025-07-03 提交的版本,查看 最新版本

// ==UserScript==
// @name         WME Places Name Normalizer
// @namespace    https://gf.qytechs.cn/en/users/mincho77
// @version      7.0.0
// @author       Mincho77 
// @description  Normaliza nombres de lugares y gestiona categorías dinámicamente en WME.
// @license      MIT
// @include      https://beta.waze.com/*
// @include      https://www.waze.com/editor*
// @include      https://www.waze.com/*/editor*
// @exclude      https://www.waze.com/user/editor*
// @grant        GM_xmlhttpRequest
// @connect      sheets.googleapis.com
// @run-at       document-end
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// ==/UserScript==
(function ()
{
    // Variables globales básicas
    const SCRIPT_NAME = GM_info.script.name;
    const VERSION = GM_info.script.version.toString();
    if (!window.swapWords) {
        const stored = localStorage.getItem("wme_swapWords");
        window.swapWords = stored ? JSON.parse(stored) : [];
    }
    // Variables globales para el panel flotante
    let floatingPanelElement = null;
    let dynamicCategoriesLoaded = false;
    const processingPanelDimensions = { width: '400px', height: '200px' };  // Panel pequeño para procesamiento
    const resultsPanelDimensions = { width: '1400px', height: '700px' };    // Panel grande para resultados
    // Variables globales para el diccionario de palabras
    const commonWords = [
        'de',  'del', 'el',    'la',   'los',  'las',  'y', 'e',
        'o',   'u',   'un',    'una',  'unos', 'unas', 'a', 'en',
    'con', 'tras', 'por',  'al',   'lo'
    ];
    // --- Definir nombres de pestañas cortos antes de la generación de botones ---
    const tabNames = [
        { label: "Gene", icon: "⚙️" },
        { label: "Espe", icon: "🏷️" },
        { label: "Dicc", icon: "📘" },
        { label: "Reemp", icon: "🔂" }
    ];
    let wmeSDK = null; // Almacena la instancia del SDK de WME.
    //Permite inicializar el SDK de WME
    function tryInitializeSDK(finalCallback)
    {
        let attempts = 0;
        const maxAttempts = 60; // Intentos máximos (60 * 500ms = 30 segundos)
        const intervalTime = 500;
        let sdkAttemptInterval = null;
        // Función interna para intentar obtener el SDK de WME
        function attempt()
        {
            if (typeof getWmeSdk === 'function')
            {
                if (sdkAttemptInterval)
                    clearInterval(sdkAttemptInterval);
                try
                {
                    wmeSDK = getWmeSdk({scriptId : 'WMEPlacesNameInspector', scriptName : SCRIPT_NAME, });
                    if (wmeSDK)
                    {
                         console.log("[SDK INIT SUCCESS] SDK obtenido exitosamente:", wmeSDK);
                    }
                    else
                    {
                        console.warn("[SDK INIT WARNING] getWmeSdk() fue llamada pero devolvió null/undefined.");
                    }
                }
                catch (e)
                {
                    console.error("[SDK INIT ERROR] Error al llamar a getWmeSdk():", e);
                    wmeSDK = null;
                }
                finalCallback();
                return;
            }
            attempts++;
            if (attempts >= maxAttempts)
            {
                if (sdkAttemptInterval) clearInterval(sdkAttemptInterval);
                // console.error(`[SDK INIT FAILURE] No se pudo encontrar getWmeSdk() después de ${maxAttempts} intentos.`);
                wmeSDK = null;
                finalCallback();
            }
        }
        sdkAttemptInterval = setInterval(attempt, intervalTime);
        attempt();
    }//tryInitializeSDK
    // Esperar a que la API principal de Waze esté completamente cargada
    async function waitForWazeAPI(callbackPrincipalDelScript) 
    {
        let wAttempts = 0;
        const wMaxAttempts = 40;
        const wInterval = setInterval(async () => { 
            wAttempts++;
            if (typeof W !== 'undefined' && W.map && W.loginManager && W.model && W.model.venues && W.userscripts &&
                typeof W.userscripts.registerSidebarTab === 'function')
            {
                clearInterval(wInterval);  
                // solo carga las categorías de Google Sheets si no se han cargado aún
                if (!dynamicCategoriesLoaded)
                { 
                    try
                    {
                        await loadDynamicCategoriesFromSheet();
                        dynamicCategoriesLoaded = true; // <-- Marcar como cargado
                    }
                    catch (error)
                    {
                        console.error("No se pudieron cargar las categorías dinámicas:", error);                        
                    }
                }
                tryInitializeSDK(callbackPrincipalDelScript);
            }
            else if (wAttempts >= wMaxAttempts)
            {
                clearInterval(wInterval);
                callbackPrincipalDelScript();
            }
        }, 500);
    }//waitforWazeAPI
    // Permite crear un panel flotante en WME
    function updateScanProgressBar(currentIndex, totalPlaces)
    {
        if (totalPlaces === 0)        
            return;
        // Calcular el porcentaje de progreso
        let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100);
        progressPercent = Math.min(progressPercent, 100);
        // Actualizar la barra de progreso
        const progressBarInnerTab = document.getElementById("progressBarInnerTab");
        const progressBarTextTab = document.getElementById("progressBarTextTab");
        // Asegurarse de que los elementos existen antes de intentar actualizarlos
        if (progressBarInnerTab && progressBarTextTab)
        {
            progressBarInnerTab.style.width = `${progressPercent}%`;
            const currentDisplay = Math.min(currentIndex + 1, totalPlaces);
            progressBarTextTab.textContent = `Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`;
        }
    }//updateScanProgressBar
    // Permite crear un panel flotante en WME
    function escapeRegExp(string)
    {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }//escapeRegExp 
    // Función para cargar palabras del diccionario desde Google Sheets (Hoja "Dictionary")
    async function loadDictionaryWordsFromSheet()
    {
        const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
        const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8"; 
        const RANGE = "Dictionary!A2:B"; 

        // Asegurarse de que window.dictionaryWords y window.dictionaryIndex estén inicializados
        if (!window.dictionaryWords) window.dictionaryWords = new Set();
        if (!window.dictionaryIndex) window.dictionaryIndex = {};

        const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;

        return new Promise((resolve) => {
            if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY") {
                console.warn('[WME PLN] SPREADSHEET_ID o API_KEY no configurados para el diccionario. Se omitirá la carga desde Google Sheets.');
                resolve();
                return;
            }

            console.log('[WME PLN] Cargando palabras del diccionario desde Google Sheets...');
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                        const data = JSON.parse(response.responseText);
                        let newWordsAdded = 0;
                        if (data.values)
                        {
                            data.values.forEach(row => {
                                const word = (row[0] || '').trim(); // Columna A es WORD
                                // const lang = (row[1] || '').trim(); // Columna B es LANG (puedes usarla si lo necesitas, por ahora no se utiliza en la lógica de 'add')

                                if (word && !window.dictionaryWords.has(word.toLowerCase())) {
                                    window.dictionaryWords.add(word.toLowerCase()); // Añadir a Set en minúsculas
                                    const firstChar = word.charAt(0).toLowerCase();
                                    if (!window.dictionaryIndex[firstChar])
                                        window.dictionaryIndex[firstChar] = [];
                                    // Asegurarse de que el índice existe para la primera letra
                                    window.dictionaryIndex[firstChar].push(word.toLowerCase()); // Añadir al índice
                                    newWordsAdded++;
                                }
                            });
                            console.log(`[WME PLN] ¡Éxito! Diccionario cargado desde Google Sheets. ${newWordsAdded} palabras nuevas añadidas.`);
                            // Guardar el diccionario actualizado en localStorage
                            try
                            {
                                localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
                            }
                            catch (e)
                            {
                                console.error("[WME PLN] Error guardando diccionario actualizado en localStorage:", e);
                            }
                        }
                        else
                        {
                            console.warn('[WME PLN] No se encontraron valores en la hoja "Dictionary".');
                        }
                    } else {
                        const error = JSON.parse(response.responseText);
                        console.error(`[WME PLN] Error al cargar la hoja "Dictionary" de Google: ${error.error.message}.`);
                    }
                    resolve();
                },
                onerror: function () {
                    console.error('Error de red al intentar conectar con Google Sheets para el diccionario.');
                    resolve();
                }
            });
        });
    }//loadDictionaryWordsFromSheet

    //Función Para Cargar Categorías Desde Google Sheets
    async function loadDynamicCategoriesFromSheet()
    {
        const SPREADSHEET_ID = "1kJDEOn8pKLdqEyhIZ9DdcrHTb_GsoeXgIN4GisrpW2Y";
        const API_KEY = "AIzaSyAQbvIQwSPNWfj6CcVEz5BmwfNkao533i8";
        const RANGE = "Categories!A2:E"; 
        // Definimos la variable global para guardar las reglas
        window.dynamicCategoryRules = [];

        const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${RANGE}?key=${API_KEY}`;
        // Verificamos si la variable global ya está definida
        return new Promise((resolve) => {
            // Medida de seguridad: si no has puesto tus datos, no intenta la conexión.
            if (SPREADSHEET_ID === "TU_SPREADSHEET_ID" || API_KEY === "TU_API_KEY")
            {
                console.warn('[WME PLN] No se ha configurado SPREADSHEET_ID o API_KEY. Se omitirá la carga de categorías dinámicas.');
                resolve(); // Permite que el script continúe sin las reglas.
                return;
            }            
            console.log('[WME PLN] Cargando reglas de categoría dinámicas desde Google Sheets...');
            GM_xmlhttpRequest({method: "GET", url: url, onload: function (response)
            {
                if (response.status >= 200 && response.status < 300)
                {
                    const data = JSON.parse(response.responseText);
                    if (data.values)
                    {
                        // Procesa los datos y los guarda en la variable global
                        window.dynamicCategoryRules = data.values.map(row => {
                            const keyword = (row[0] || '').toLowerCase().trim();
                            const keywords = keyword.split(';').map(k => k.trim()).filter(k => k.length > 0);
                            const regexParts = keywords.map(k => `\\b${escapeRegExp(k)}\\b`);
                            const combinedRegex = new RegExp(`(${regexParts.join('|')})`, 'i');
                            return {
                                keyword: keyword, // Mantener original si es necesario
                                categoryKey: row[1] || '',
                                icon: row[2] || '⚪',
                                desc_es: row[3] || 'Sin descripción',
                                desc_en: row[4] || 'No description',
                                compiledRegex: combinedRegex // Guarda la regex pre-compilada
                            };
                        });
                        // Una vez cargadas, ordena las reglas UNA SOLA VEZ
                        window.dynamicCategoryRules.sort((a, b) => b.keyword.length - a.keyword.length);
                        console.log('[WME PLN] ¡Éxito! Reglas de categoría dinámicas cargadas y ordenadas:', window.dynamicCategoryRules);
                    }
                    else
                    {
                        console.warn('[WME PLN] No se encontraron valores en la hoja de categorías.');
                    }
                }
                else
                {
                    const error = JSON.parse(response.responseText);
                    alert(`Error al cargar la hoja de Google: ${error.error.message}.`);
                }
                resolve();
                }, onerror: function ()
                {
                    alert('Error de red al intentar conectar con Google Sheets.');
                    resolve();
                }
            });// GM_xmlhttpRequest
        });// loadDynamicCategoriesFromSheet
    }//loadDynamicCategoriesFromSheet

    // Función para encontrar la categoría de un lugar basado en su nombre
    function findCategoryForPlace(placeName)
    {
        // Si el nombre del lugar es inválido o no hay reglas de categoría cargadas,
        // devuelve un array vacío de sugerencias.
        if (!placeName || typeof placeName !== 'string' || !window.dynamicCategoryRules || window.dynamicCategoryRules.length === 0)
            return [];
        const lowerCasePlaceName = placeName.toLowerCase();
        const allMatchingRules = []; // Este array almacenará todas las reglas de categoría que coincidan.
        const placeWords = lowerCasePlaceName.split(/\s+/).filter(w => w.length > 0); // Descomponer el nombre del lugar en palabras
        const SIMILARITY_THRESHOLD_FOR_KEYWORDS = 0.95; // Puedes ajustar este umbral (ej. 0.90 para 90% de similitud)
        // Las reglas ya están ordenadas globalmente por loadDynamicCategoriesFromSheet
        // (que ordena por keyword.length descendente), así que no necesitamos ordenarlas aquí de nuevo.
        for (const rule of window.dynamicCategoryRules)
        {
            // Si la regla no tiene una expresión regular compilada (lo cual no debería pasar si se cargó correctamente),
            // salta a la siguiente regla.
            if (!rule.compiledRegex) continue;
             // **PASO 1: Búsqueda por Regex Exacta 
            if (rule.compiledRegex.test(lowerCasePlaceName))
            {
                if (!allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey)) {
                    allMatchingRules.push(rule);
                }
                // SI YA AÑADIMOS LA REGLA POR REGEX EXACTA, PASAR A LA SIGUIENTE REGLA PARA AHORRAR CÁLCULOS DE SIMILITUD
                continue; 
            }
            // **PASO 2: Búsqueda por Similitud para CADA palabra del lugar vs CADA palabra clave de la regla**
            // Descomponer la 'keyword' de la regla en sus palabras individuales (si usa ';')
            const ruleKeywords = rule.keyword.split(';').map(k => k.trim().toLowerCase()).filter(k => k.length > 0);

            let foundSimilarityForThisRule = false;
            for (const pWord of placeWords)
            { // Cada palabra del nombre del lugar
                if (foundSimilarityForThisRule) break; // Si ya encontramos una buena similitud para esta regla, pasamos a la siguiente.

                for (const rKeyword of ruleKeywords)
                { // Cada palabra clave de la regla
                    // Asegurarse de que rKeyword no sea una expresión regular, sino la palabra literal para Levenshtein
                    const similarity = calculateSimilarity(pWord, rKeyword); //
                    
                    // Si la similitud es alta y aún no hemos añadido esta categoría
                    if (similarity >= SIMILARITY_THRESHOLD_FOR_KEYWORDS && !allMatchingRules.some(mr => mr.categoryKey === rule.categoryKey))
                    {
                        allMatchingRules.push(rule);
                        foundSimilarityForThisRule = true; // Marcamos que ya la encontramos para esta regla
                        break; // Salimos del bucle de rKeyword y pWord
                    }
                }
            }
        }
//console.log(`[WME PLN DEBUG] findCategoryForPlace para "${placeName}" devolvió: `, allMatchingRules);
        return allMatchingRules;
    }//findCategoryForPlace

    // Permite obtener el icono de una categoría
    function getWazeLanguage()
    {
        // 1. Intento principal con el SDK (método recomendado)
        if (wmeSDK && typeof wmeSDK.getWazeLocale === 'function')
        {
            const locale = wmeSDK.getWazeLocale(); // ej: 'es-419'
            if (locale) 
                return locale.split('-')[0].toLowerCase(); // -> 'es'            
        }
        // 2. Fallback al objeto global 'W' si el SDK falla
        if (typeof W !== 'undefined' && W.locale) 
            return W.locale.split('-')[0].toLowerCase();        
        // 3. Último recurso si nada funciona
        return 'es';
    }//getWazeLanguage

    //Permite obtener el icono y descripción de una categoría
    function getCategoryDetails(categoryKey)
    {
        const lang = getWazeLanguage();
        // 1. Intento con la hoja de Google (window.dynamicCategoryRules)
        if (window.dynamicCategoryRules && window.dynamicCategoryRules.length > 0) {
            const rule = window.dynamicCategoryRules.find(r => r.categoryKey.toUpperCase() === categoryKey.toUpperCase());
            if (rule) {
                const description = (lang === 'es' && rule.desc_es) ? rule.desc_es : rule.desc_en;
                return { icon: rule.icon, description: description };
            }
        }
        // 2. Fallback a la lista interna del script si no se encontró en la hoja
        const hardcodedInfo = getCategoryIcon(categoryKey); // Llama a la función original
        if (hardcodedInfo && hardcodedInfo.icon !== '⚪' && hardcodedInfo.icon !== '❓') {
             // La función original devuelve un título "Español / English", lo separamos.
            const descriptions = hardcodedInfo.title.split(' / ');
            const description = (lang === 'es' && descriptions[0]) ? descriptions[0] : descriptions[1] || descriptions[0];
            return { icon: hardcodedInfo.icon, description: description };
        }
        // 3. Si no se encuentra en ninguna parte, devolver un valor por defecto.
        const defaultDescription = lang === 'es' ? `Categoría no encontrada (${categoryKey})` : `Category not found (${categoryKey})`;
        return { icon: '⚪', description: defaultDescription };
    }//getCategoryDetails

    // Función para eliminar diacríticos de una cadena
    function removeDiacritics(str)
    {
        return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
    }//removeDiacritics

    // Función para validar una palabra excluida
    function isValidExcludedWord(newWord)
    {
        if (!newWord)
            return { valid : false, msg : "La palabra no puede estar vacía." };
        const lowerNewWord = newWord.toLowerCase();
        // No permitir palabras de un solo caracter
        if (newWord.length === 1)        
            return { valid : false,  msg : "No se permite agregar palabras de un solo caracter." };
        // No permitir caracteres especiales solos
        if (/^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]+$/.test(newWord))        
            return { valid : false, msg : "No se permite agregar solo caracteres especiales." };
        // No permitir si ya está en el diccionario (ignorando mayúsculas)
        if (window.dictionaryWords && Array.from(window.dictionaryWords).some(w => w.toLowerCase() === lowerNewWord))        
            return { valid : false, msg :"La palabra ya existe en el diccionario. No se puede agregar a especiales." };        
        // No permitir palabras comunes
        if (commonWords.includes(lowerNewWord))        
            return { valid : false, msg : "Esa palabra es muy común y no debe agregarse a la lista." };        
        // No permitir duplicados en excluidas
        if (excludedWords && Array.from(excludedWords).some(w => w.toLowerCase() === lowerNewWord))        
            return { valid : false, msg : "La palabra ya está en la lista (ignorando mayúsculas)." };
        return { valid : true };
    }//isValidExcludeWord

    // La función removeEmoticons con una regex más segura o un paso extraremoveEmoticons solo para emojis (sin afectar números)
    function removeEmoticons(text)
    {
        if (!text || typeof text !== 'string')        
            return '';
        const specificEmojiAndSymbolRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\udff5]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udf00-\udfff]|\ud83d[\udc00-\udfff]|\ud83e[\udc00-\udfff]|[\u2600-\u26FF]\ufe0f?)/gu;
        let cleanedText = text.replace(specificEmojiAndSymbolRegex, '');        
        // Si 🔋 sigue sin quitarse, añade su reemplazo explícito:
        cleanedText = cleanedText.replace(/\uD83D\uDD0B/g, ''); // Unicode para 🔋 (U+1F50B)
        return cleanedText.trim().replace(/\s{2,}/g, ' ');
    }// removeEmoticons

    // Modify aplicarReemplazosGenerales
    function aplicarReemplazosGenerales(name)
    {
        if (typeof window.skipGeneralReplacements === "boolean" && window.skipGeneralReplacements) 
            return name;        
        // Paso 1: Eliminar emoticones al inicio de los reemplazos generales. 
        name = removeEmoticons(name);
        const reglas = [
            // Nueva regla: reemplazar | por espacio, guion y espacio
            { buscar: /\|/g, reemplazar: " - " },
            // Nueva regla: reemplazar / por espacio, barra y espacio, eliminando espacios alrededor
            { buscar: /\s*\/\s*/g, reemplazar: " / " },
            // Corrección: Para buscar [P] o [p] literalmente
            { buscar: /\[[Pp]\]/g, reemplazar: "" },
            { buscar: /\s*-\s*/g, reemplazar: " - " },
        ];
        reglas.forEach(regla => {
            if (regla.buscar.source === '\\|') {
                name = name.replace(regla.buscar, regla.reemplazar);
            } else {
                name = name.replace(regla.buscar, regla.reemplazar);
            }
        });
        name = name.replace(/\s{2,}/g, ' ').trim(); // Asegura el recorte final y espacios únicos
        return name;
    }
    //Permite aplicar reglas especiales de capitalización y puntuación a un nombre
    function aplicarReglasEspecialesNombre(newName)
    {
        newName = newName.replace(/([A-Za-z])'([A-Za-z])/g, (match, before, after) => `${before}'${after.toLowerCase()}`);
        newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`);
        newName = newName.replace(/\.\s+([a-z])/g, (match, letter) => `. ${letter.toUpperCase()}`);
        //Capitalizar Después De Paréntesis De Apertura 
        newName = newName.replace(/(\(\s*)([a-z])/g, (match, P1, P2) => {
                // P1 es el paréntesis de apertura y cualquier espacio después (ej. "( " o "(")
                // P2 es la primera letra minúscula después de eso.
                return P1 + P2.toUpperCase();
            }
        );
        newName = newName.replace(/\s([a-zA-Z])$/, (match, letter) => ` ${letter.toUpperCase()}`);
//console.log("[DEBUG aplicarReglasEspecialesNombre] Before hyphen capitalization:", newName);
        newName = newName.replace(/-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`);
//console.log("[DEBUG aplicarReglasEspecialesNombre] After hyphen capitalization:", newName);
        return newName;
    }
    //Permite aplicar reemplazos definidos por el usuario a un texto
    function aplicarReemplazosDefinidos(text, replacementRules)
    {
        let newText = text;
        // Verificar si replacementRules es un objeto y tiene claves
        if (typeof replacementRules !== 'object' || replacementRules === null || Object.keys(replacementRules).length === 0)        
            return newText; // No hay reglas o no es un objeto, devolver el texto original        
        // Ordenar las claves de reemplazo por longitud descendente para manejar correctamente
        // los casos donde una clave puede ser subcadena de otra (ej. "D1 Super" y "D1").
        const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);
        for (const fromKey of sortedFromKeys)
        {
            const toValue = replacementRules[fromKey];
            const escapedFromKey = escapeRegExp(fromKey);
            // Regex mejorada para definir "palabra completa" usando propiedades Unicode.
            // Grupo 1: Captura el delimitador previo (un no-palabra o inicio de cadena).
            // Grupo 2: Captura la clave de reemplazo que coincidió.
            // Grupo 3: Captura el delimitador posterior o fin de cadena.
            const regex = new RegExp(`(^|[^\\p{L}\\p{N}_])(${escapedFromKey})($|(?=[^\\p{L}\\p{N}_]))`, 'giu');
            newText = newText.replace(regex, (
                match,                          // La subcadena coincidente completa
                delimitadorPrevio,              // Contenido del grupo de captura 1
                matchedKey,                     // Contenido del grupo de captura 2
                _delimitadorPosteriorOculto,    // Contenido del grupo de captura 3 (no usado directamente en la lógica de abajo, pero necesario para alinear parámetros)
                offsetOfMatchInCurrentText,     // El desplazamiento de la subcadena coincidente
                stringBeingProcessed            // La cadena completa que se está examinando
            ) => {
            // Asegurarse de que stringBeingProcessed es una cadena
            if (typeof stringBeingProcessed !== 'string')
            {
                 console.error("[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'stringBeingProcessed' (la cadena original en el reemplazo) no es una cadena.",
                    "Tipo:", typeof stringBeingProcessed, "Valor:", stringBeingProcessed,
                    "Regla actual (fromKey):", fromKey, "Texto parcial procesado:", newText
                );
                return match;
            }
            // Asegurarse de que offsetOfMatchInCurrentText es un número
            if (typeof offsetOfMatchInCurrentText !== 'number')
            {
                console.error("[WME PLN Error] aplicarReemplazosDefinidos: el argumento 'offsetOfMatchInCurrentText' no es un número.",
                    "Tipo:", typeof offsetOfMatchInCurrentText, "Valor:", offsetOfMatchInCurrentText
                );
                return match;
            }
            // Lógica existente para evitar el reemplazo si toValue ya está presente contextualizando fromKey.
            const fromKeyLower = fromKey.toLowerCase();
            const toValueLower = toValue.toLowerCase();
            const indexOfFromInTo = toValueLower.indexOf(fromKeyLower);
            // Si fromKeyLower está presente en toValueLower, verificar si ya existe en el texto original
            if (indexOfFromInTo !== -1)
            {
                // El offset real de matchedKey dentro de stringBeingProcessed
                const actualMatchedKeyOffset = offsetOfMatchInCurrentText + (delimitadorPrevio ? delimitadorPrevio.length : 0);
                const potentialExistingToStart = actualMatchedKeyOffset - indexOfFromInTo;

                if (potentialExistingToStart >= 0 && (potentialExistingToStart + toValue.length) <= stringBeingProcessed.length)
                {
                    const substringInOriginal = stringBeingProcessed.substring(potentialExistingToStart, potentialExistingToStart + toValue.length);
                    if (substringInOriginal.toLowerCase() === toValueLower)
                    {
                        return match;
                    }
                }
            }

            // Evitar Duplicación De Palabras En Los Bordes
            const palabrasDelToValue = toValue.trim().split(/\s+/).filter(p => p.length >0); // Filtrar palabras vacías
            if (palabrasDelToValue.length > 0)
            {
                const primeraPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[0].toLowerCase());
                const ultimaPalabraToValueLimpia = removeDiacritics(palabrasDelToValue[palabrasDelToValue.length - 1].toLowerCase());
                // Palabra ANTES del inicio del 'match' (delimitadorPrevio + matchedKey)
                // El offsetOfMatchInCurrentText es el inicio de 'match', no de 'delimitadorPrevio' si son diferentes.
                // Necesitamos el texto ANTES de delimitadorPrevio. El offset real del inicio del match completo es offsetOfMatchInCurrentText.
                const textoAntesDelMatch = stringBeingProcessed.substring(0, offsetOfMatchInCurrentText);
                const palabrasEnTextoAntes = textoAntesDelMatch.trim().split(/\s+/).filter(p => p.length > 0);
                const palabraAnteriorLimpia = palabrasEnTextoAntes.length > 0 ? removeDiacritics(palabrasEnTextoAntes[palabrasEnTextoAntes.length - 1].toLowerCase()) : "";
                // Palabra DESPUÉS del final del 'match' (delimitadorPrevio + matchedKey)
                const textoDespuesDelMatch = stringBeingProcessed.substring(offsetOfMatchInCurrentText + match.length);
                const palabrasEnTextoDespues = textoDespuesDelMatch.trim().split(/\s+/).filter(p => p.length > 0);
                const palabraSiguienteLimpia = palabrasEnTextoDespues.length > 0 ? removeDiacritics(palabrasEnTextoDespues[0].toLowerCase()) : "";
                // Delimitador posterior oculto: lo que sigue inmediatamente al matchedKey
                if (palabraAnteriorLimpia && primeraPalabraToValueLimpia && palabraAnteriorLimpia === primeraPalabraToValueLimpia)
                {
                        // Solo prevenir si el delimitador previo es solo espacio o vacío,
                        // indicando adyacencia real de palabras.
                    if (delimitadorPrevio.trim() === "" || delimitadorPrevio.match(/^\s+$/))
                    {
                        return match;
                    }
                }
                //Evitar duplicación de la última palabra del toValue
                if (palabraSiguienteLimpia && ultimaPalabraToValueLimpia && ultimaPalabraToValueLimpia === palabraSiguienteLimpia)
                {
                    // El delimitadorPosteriorOculto nos dice qué sigue inmediatamente al matchedKey.
                    // Si este delimitador es solo espacio o vacío, y luego viene la palabra duplicada.
                    if (_delimitadorPosteriorOculto.trim() === "" || _delimitadorPosteriorOculto.match(/^\s+$/))                        
                        return match;
                    
                }
            }
            return delimitadorPrevio + toValue;
        });
    }
    return newText;
}
    //Permite crear un panel flotante en WME
    function getVisiblePlaces()
    {
        if (typeof W === 'undefined' || !W.map || !W.model || !W.model.venues)
        {
            console.warn('Waze Map Editor no está completamente cargado.');
            return [];
        }

        const venues = W.model.venues.objects;
        const visiblePlaces = Object.values(venues).filter(venue => {
            const olGeometry = venue.getOLGeometry?.();
            const bounds = olGeometry?.getBounds?.();
            return bounds && W.map.getExtent().intersectsBounds(bounds);
        });
        return visiblePlaces;
    }
    //Permite renderizar los lugares en el panel flotante
    function renderPlacesInFloatingPanel(places)
    {
        createFloatingPanel("processing"); // Mostrar panel en modo "procesando"
        const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10);
        if (places.length > maxPlacesToScan)
        {
            places = places.slice(0, maxPlacesToScan); // Limitar el número de places a escanear
        }        
        // Permite obtener el nombre de la categoría de un lugar, ya sea del modelo antiguo o del SDK
        function getPlaceCategoryName(venueFromOldModel, venueSDKObject)
        { // Acepta ambos tipos de venue
            let categoryId = null;
            let categoryName = null;
            // Intento 1: Usar el venueSDKObject si está disponible y tiene la info
            if (venueSDKObject)
            {
                if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id)
                {
                    categoryId = venueSDKObject.mainCategory.id;
                    if (venueSDKObject.mainCategory.name)
                    {
                        categoryName = venueSDKObject.mainCategory.name;
                        // source = "SDK (mainCategory.name)";
                    }
                }
                else if (Array.isArray(venueSDKObject.categories) && venueSDKObject.categories.length > 0)
                {
                    const firstCategorySDK = venueSDKObject.categories[0];
                    if (typeof firstCategorySDK === 'object' && firstCategorySDK.id)
                    {
                        categoryId = firstCategorySDK.id;
                        if (firstCategorySDK.name)
                        {
                            categoryName = firstCategorySDK.name;
                            // source = "SDK (categories[0].name)";
                        }
                    }
                    else if (typeof firstCategorySDK === 'string')
                    {
                        categoryName = firstCategorySDK;
                        // source = "SDK (categories[0] as string)";
                    }
                }
                else if (venueSDKObject.primaryCategoryID)
                {
                    categoryId = venueSDKObject.primaryCategoryID;
                    // source = "SDK (primaryCategoryID)";
                }
            }

            if (categoryName)
            {
                // console.log(`[CATEGORÍA] Usando nombre de categoría de ${source}: ${categoryName} ${categoryId ? `(ID: ${categoryId})` : ''}`); // Comentario de depuración eliminado
                return categoryName;
            }

            // Intento 2: Usar W.model si no se obtuvo del SDK
            if (!categoryId && venueFromOldModel && venueFromOldModel.attributes && Array.isArray(venueFromOldModel.attributes.categories) && venueFromOldModel.attributes.categories.length > 0)
            {
                categoryId = venueFromOldModel.attributes.categories[0];
                // source = "W.model (attributes.categories[0])";
            }

            if (!categoryId)
            {
                return "Sin categoría";
            }

            let categoryObjWModel = null;
            if (typeof W !== 'undefined' && W.model)
            {
                if (W.model.venueCategories &&
                    typeof W.model.venueCategories.getObjectById === "function")
                {
                    categoryObjWModel =
                      W.model.venueCategories.getObjectById(categoryId);
                }
                if (!categoryObjWModel && W.model.categories &&
                    typeof W.model.categories.getObjectById === "function")
                {
                    categoryObjWModel =
                      W.model.categories.getObjectById(categoryId);
                }
            }

            if (categoryObjWModel && categoryObjWModel.attributes &&
                categoryObjWModel.attributes.name)
            {
                // console.log(`[CATEGORÍA] Usando nombre de categoría de W.model.categories (para ID ${categoryId} de ${source}): ${categoryObjWModel.attributes.name}`); // Comentario de depuración eliminado
                return categoryObjWModel.attributes.name;
            }

            if (typeof categoryId === 'number' ||
                (typeof categoryId === 'string' && categoryId.trim() !== ''))
            {
                // console.log(`[CATEGORÍA] No se pudo resolver el nombre para ID de categoría ${categoryId} (obtenido de ${source}). Devolviendo ID.`); // Comentario de depuración eliminado
                return `${categoryId}`; // Devuelve el ID si no se encuentra el nombre.
            }
            return "Sin categoría";
        }
        //Permite obtener el tipo de lugar (área o punto) y su icono
        function getPlaceTypeInfo(venue)
        {
            const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null;
            const isArea = geometry?.CLASS_NAME?.endsWith("Polygon");
            return {isArea, icon : isArea ? "⭔" : "⊙", title : isArea ? "Área" : "Punto"};
        }
    //Fin de funciones auxiliares para tipo y categoría
        //Permite procesar un lugar y generar un objeto con sus detalles
    function shouldForceSuggestionForReview(word)
    {
        if (typeof word !== 'string')
        {
            return false;
        }
        const lowerWord = word.toLowerCase();
        // Verificar si la palabra tiene alguna tilde (incluyendo mayúsculas acentuadas)
        const hasTilde = /[áéíóúÁÉÍÓÚ]/.test(word);
        // Si no tiene tilde, no forzar sugerencia por esta regla
        if (!hasTilde)
        {
            return false; // Si no hay tilde, no forzar sugerencia por esta regla
        }
        // Lista de patrones de letras/combinaciones que, junto con una tilde, fuerzan la sugerencia
        // (insensible a mayúsculas debido a lowerWord)
        const problematicSubstrings = ['c', 's', 'x', 'cc', 'sc', 'cs', 'g', 'j', 'z', 'ch', 'qu', 'll', 'ñ', 'rr'];
        // Verificar si la palabra contiene alguna de las letras/combinaciones problemáticas
        for (const sub of problematicSubstrings)
        {
            if (lowerWord.includes(sub))            
                return true; // Tiene tilde y una de las letras/combinaciones problemáticas
            
        }
        return false; // Tiene tilde, pero no una de las letras/combinaciones problemáticas
    }
    //Permite procesar un lugar y generar un objeto con sus detalles
    function getLoggedInUserInfo()
    {
        // Verificar si W, W.loginManager y W.loginManager.user están definidos
        if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user)
        {
            const user = W.loginManager.user;
            const userInfo = {};
            // Verificar si user tiene las propiedades id y userName
            if (typeof user.id === 'number')
            {
                userInfo.userId = user.id;
            }
            else
            {
                userInfo.userId = null;
            }
            // Verificar si user tiene la propiedad userName y es una cadena no vacía
            if (typeof user.userName === 'string' && user.userName.trim() !== '')
            {
                userInfo.userName = user.userName;
            }
            else
            {
                userInfo.userName = null;
            }
            // Devuelve el objeto solo si se pudo obtener al menos un dato
            if (userInfo.userId !== null || userInfo.userName !== null) {
                return userInfo;
            }
        }
        // Si W, W.loginManager o W.loginManager.user no están definidos,
        // o no se pudo obtener ni ID ni userName.
        console.warn("[WME PLN] No se pudo obtener la información del usuario logueado.");
        return null;
    }


        //****************************************************************************************************
        // Nombre: getPlaceCityInfo
        // Autor: mincho77
        // Fecha: 2025-05-28 // Actualizado 2025-05-29
        // Descripción: Determina si un lugar tiene una ciudad asignada o información de calle y devuelve información iconográfica.
        // Parámetros:
        //   venueFromOldModel (Object): El objeto 'venue' del modelo antiguo de WME.
        //   venueSDKObject (Object, opcional): El objeto 'venue' del SDK de WME.
        // Retorna:
        //   Object: Un objeto con 'icon' (String), 'title' (String) y 'hasCity' (boolean indicando ciudad explícita).
        //****************************************************************************************************
        async function getPlaceCityInfo(venueFromOldModel, venueSDKObject)
        {
            let hasExplicitCity = false;
            let explicitCityName = null;

            let hasStreetInfo = false;
            let cityAssociatedWithStreet = null;

            // --- 1. Check for EXPLICIT city ---
            // SDK
            if (venueSDKObject && venueSDKObject.address)
            {
                if (venueSDKObject.address.city && typeof venueSDKObject.address.city.name === 'string' && venueSDKObject.address.city.name.trim() !== '')
                {
                    explicitCityName = venueSDKObject.address.city.name.trim();
                    hasExplicitCity = true;
                    // source = "SDK (address.city.name)";
                }
                else if (typeof venueSDKObject.address.cityName === 'string' && venueSDKObject.address.cityName.trim() !== '')
                {
                    explicitCityName = venueSDKObject.address.cityName.trim();
                    hasExplicitCity = true;
                    // source = "SDK (address.cityName)";
                }
            }
            // Old Model (if no explicit city from SDK)
            if (!hasExplicitCity && venueFromOldModel && venueFromOldModel.attributes)
            {
                const cityID = venueFromOldModel.attributes.cityID;
                if (cityID && typeof W !== 'undefined' && W.model && W.model.cities && W.model.cities.getObjectById)
                {
                    const cityObject = W.model.cities.getObjectById(cityID);
                    if (cityObject && cityObject.attributes && typeof cityObject.attributes.name === 'string' && cityObject.attributes.name.trim() !== '')
                    {
                        explicitCityName = cityObject.attributes.name.trim();
                        hasExplicitCity = true;
                        // source = `W.model.cities (ID: ${cityID})`;
                    }
                }
            }

            // --- 2. Check for STREET information (and any city derived from it) ---
            // SDK street check
            if (venueSDKObject && venueSDKObject.address)
            {
                if ((venueSDKObject.address.street && typeof venueSDKObject.address.street.name === 'string' && venueSDKObject.address.street.name.trim() !== '') ||
                    (typeof venueSDKObject.address.streetName === 'string' && venueSDKObject.address.streetName.trim() !== ''))
                {
                    hasStreetInfo = true;
                    // source += (source ? ", " : "") + "SDK (street info)";
                }
            }

            // Old Model street check (if not found via SDK or to supplement)
            if (venueFromOldModel && venueFromOldModel.attributes && venueFromOldModel.attributes.streetID)
            {
                hasStreetInfo = true; // Street ID exists in old model
                const streetID = venueFromOldModel.attributes.streetID;
                if (typeof W !== 'undefined' && W.model && W.model.streets && W.model.streets.getObjectById)
                {
                    const streetObject = W.model.streets.getObjectById(streetID);
                    if (streetObject && streetObject.attributes && streetObject.attributes.cityID)
                    {
                        const cityIDFromStreet = streetObject.attributes.cityID;
                        if (W.model.cities && W.model.cities.getObjectById)
                        {
                            const cityObjectFromStreet = W.model.cities.getObjectById(cityIDFromStreet);
                            if (cityObjectFromStreet && cityObjectFromStreet.attributes && typeof cityObjectFromStreet.attributes.name === 'string' && cityObjectFromStreet.attributes.name.trim() !== '')
                            {
                                cityAssociatedWithStreet = cityObjectFromStreet.attributes.name.trim();
                                // source += (source ? ", " : "") + `W.model.streets -> W.model.cities (StreetID: ${streetID}, CityID: ${cityIDFromStreet} -> ${cityAssociatedWithStreet})`;
                            }
                        }
                    }
                }
            }
            // --- 3. Determine icon, title, and returned hasCity based on user's specified logic ---
            let icon;
            let title;
            const returnedHasCityBoolean = hasExplicitCity; // To be returned, indicates if an *explicit* city is set.
            const hasAnyAddressInfo = hasExplicitCity || hasStreetInfo;
//console.log(`[WME PLN DEBUG CityInfo] Calculated flags: hasExplicitCity=${hasExplicitCity} (Name: ${explicitCityName}), hasStreetInfo=${hasStreetInfo}, cityAssociatedWithStreet=${cityAssociatedWithStreet}`);
//console.log(`[WME PLN DEBUG CityInfo] Calculated: hasAnyAddressInfo=${hasAnyAddressInfo}`);
            if (hasAnyAddressInfo)
            {
                if (hasExplicitCity)
                {
                    icon = "🏙️"; // Icono para ciudad asignada
                    title = `Ciudad: ${explicitCityName}`;
                }
                else
                { // No tiene ciudad explícita, pero sí información de calle
                    icon = "🛣️"; // Icono para "tiene calle pero no ciudad explícita"
                    if (cityAssociatedWithStreet)
                    {
                        title = `Tiene ciudad asociada a la calle: ${cityAssociatedWithStreet}`;
                    }
                    else
                    {
                        title = "Tiene calle, sin ciudad explícita";
                    }
                }
            }
            else
            { // No tiene ni ciudad explícita ni información de calle
                icon = "🚫";
                title = "Sin info. de dirección (ni ciudad ni calle)";
            }
            // console.log(`[CITY INFO] Place ID ${venueFromOldModel ? venueFromOldModel.getID() : 'N/A'}: Icon=${icon}, Title='${title}', HasExplicitCity=${hasExplicitCity}, HasStreet=${hasStreetInfo}, CityViaStreet='${cityAssociatedWithStreet}', ReturnedHasCity=${returnedHasCityBoolean}`);
            return {
                icon: icon,
                title: title,
                hasCity: returnedHasCityBoolean // Este booleano se refiere a si hay una ciudad *explícitamente* seleccionada.
            };
        }
        // --- Renderizar barra de progreso en el TAB PRINCIPAL justo después del slice ---
        const tabOutput = document.querySelector("#wme-normalization-tab-output");
        if (tabOutput)
        {
            // Reiniciar el estilo del mensaje en el tab al valor predeterminado
            tabOutput.style.color = "#000";
            tabOutput.style.fontWeight = "normal";
            // Crear barra de progreso visual
            const progressBarWrapperTab = document.createElement("div");
            progressBarWrapperTab.style.margin = "10px 0";
            progressBarWrapperTab.style.marginTop = "10px";
            progressBarWrapperTab.style.height = "18px";
            progressBarWrapperTab.style.backgroundColor = "transparent";

            const progressBarTab = document.createElement("div");
            progressBarTab.style.height = "100%";
            progressBarTab.style.width = "0%";
            progressBarTab.style.backgroundColor = "#007bff";
            progressBarTab.style.transition = "width 0.2s";
            progressBarTab.id = "progressBarInnerTab";
            progressBarWrapperTab.appendChild(progressBarTab);

            const progressTextTab = document.createElement("div");
            progressTextTab.style.fontSize = "12px";
            progressTextTab.style.marginTop = "5px";
            progressTextTab.id = "progressBarTextTab";
            tabOutput.appendChild(progressBarWrapperTab);
            tabOutput.appendChild(progressTextTab);
        }

        // Asegurar que la barra de progreso en el tab se actualice desde el
        // principio
        const progressBarInnerTab = document.getElementById("progressBarInnerTab");
        const progressBarTextTab = document.getElementById("progressBarTextTab");
        if (progressBarInnerTab && progressBarTextTab)
        {
            progressBarInnerTab.style.width = "0%";
            progressBarTextTab.textContent = `Progreso: 0% (0/${places.length})`;
        }

        // Mostrar el panel flotante desde el inicio
        // --- PANEL FLOTANTE: limpiar y preparar salida ---
        const output = document.querySelector("#wme-place-inspector-output");
        if (!output)
        {
            console.error("❌ Panel flotante no está disponible");
            return;
        }
        output.innerHTML = ""; // Limpia completamente el contenido del panel flotante
        output.innerHTML = "<div style='display:flex; align-items:center; gap:10px;'><span class='loader-spinner' style='width:16px; height:16px; border:2px solid #ccc; border-top:2px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span><div><div id='processingText'>Procesando lugares visibles<span class='dots'>.</span></div><div id='processingStep' style='font-size:11px; color:#555;'>Inicializando escaneo...</div></div></div>";

        // Animación de puntos suspensivos
        const dotsSpan = output.querySelector(".dots");
        if (dotsSpan)
        {
            const dotStates = ["", ".", "..", "..."];
            let dotIndex = 0;
            window.processingDotsInterval = setInterval(() => {
                dotIndex = (dotIndex + 1) % dotStates.length;
                dotsSpan.textContent = dotStates[dotIndex];
            }, 500);
        }
        output.style.height = "calc(55vh - 40px)";

        // Si no hay places, mostrar mensaje y salir
        if (!places.length)
        {
            output.appendChild(document.createTextNode("No hay places visibles para analizar."));
            const existingOverlay = document.getElementById("scanSpinnerOverlay");
            if (existingOverlay)
                existingOverlay.remove();
            return;
        }

        // --- Procesamiento incremental para evitar congelamiento ---
        let inconsistents = [];
        let index = 0;

        // Remover ícono de ✔ previo si existe
        const scanBtn = document.querySelector("button[type='button']");
        if (scanBtn)
        {
            const existingCheck = scanBtn.querySelector("span");
            if (existingCheck)
            {
                existingCheck.remove();
            }
        }
        // --- Sugerencias por palabra global para toda la ejecución ---
        let sugerenciasPorPalabra = {};
        // Convertir excludedWords a array solo una vez al inicio del análisis,
        // seguro ante undefined
        const excludedArray = (typeof excludedWords !== "undefined" && Array.isArray(excludedWords)) ? excludedWords : (typeof excludedWords !== "undefined" ? Array.from(excludedWords) : []);

        //Función para actualizar la barra de progreso
        async function processNextPlace()
        {
            const currentPlaceForLog = places[index];
            const currentVenueIdForLog = currentPlaceForLog ? currentPlaceForLog.getID() : 'ID Desconocido';
            // --- Obtener venueSDK lo antes posible para el nombre más confiable ---              
            let venueSDK = null;
            if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues && wmeSDK.DataModel.Venues.getById) {
                try {
                    venueSDK = await wmeSDK.DataModel.Venues.getById({ venueId: currentVenueIdForLog });
                } catch (sdkError) {
                    console.error(`[WME_PLN_TRACE] Error al obtener venueSDK para ID ${currentVenueIdForLog}:`, sdkError);
                }
            }
            // --- Determinar el nombre original más completo (priorizando SDK) ---
            let originalNameRaw; // Declaramos aquí, se asigna en el if/else
            if (venueSDK && venueSDK.name)
            {
                originalNameRaw = venueSDK.name; // Si el SDK tiene nombre, úsalo.
            }
            else
            {
                // Fallback al nombre del modelo antiguo
                originalNameRaw = currentPlaceForLog && currentPlaceForLog.attributes ? 
                                  (currentPlaceForLog.attributes.name?.value || currentPlaceForLog.attributes.name || '') : 
                                  '';
            }
            originalNameRaw = originalNameRaw.trim(); // Trim lo antes posible en la versión más "cruda".
            // AHORA sí, aplica removeEmoticons UNA SOLA VEZ al nombre "crudo"
            let originalNameFull = removeEmoticons(originalNameRaw); 
            // logs de depuración para confirmar los valores en este punto
//  console.log(`[DEBUG - INICIO] originalNameRaw (obtenido de Waze/SDK y trimmeado): "${originalNameRaw}"`);
//console.log(`[DEBUG - INICIO] originalNameFull (después de removeEmoticons - la única aplicación): "${originalNameFull}"`);
            // 1. Leer estados de checkboxes y configuraciones iniciales
           // console.log(`[WME_PLN_TRACE] Leyendo configuraciones...`);
            const useFullPipeline = true;
            const applyGeneralReplacements = useFullPipeline || (document.getElementById("chk-general-replacements")?.checked ?? true);
            const checkExcludedWords = useFullPipeline || (document.getElementById("chk-check-excluded")?.checked ?? false);
            const checkDictionaryWords = true;
            const restoreCommas = document.getElementById("chk-restore-commas")?.checked ?? false;
            const similarityThreshold = parseFloat(document.getElementById("similarityThreshold")?.value || "85") / 100;
            //console.log(`[WME_PLN_TRACE] Configuraciones leídas.`);
            // 2. Condición de salida principal (todos los lugares procesados)
            if (index >= places.length)
            {
               // console.log("[WME_PLN_TRACE] Todos los lugares procesados. Finalizando render...");
                finalizeRender(inconsistents, places);
                return;
            }
            const venueFromOldModel = places[index];
            const currentVenueNameObj = venueFromOldModel?.attributes?.name;
            const nameValue = typeof currentVenueNameObj === 'object' && currentVenueNameObj !== null &&
                      typeof currentVenueNameObj.value === 'string' ? currentVenueNameObj.value.trim() !== ''
                            ? currentVenueNameObj.value : undefined : typeof currentVenueNameObj === 'string' &&
                          currentVenueNameObj.trim() !== '' ? currentVenueNameObj : undefined;
            if (!places[index] || typeof places[index] !== 'object')
            {
               // console.warn(`[WME_PLN_TRACE] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]);
                updateScanProgressBar(index, places.length);
                index++;
               // console.log(`[WME_PLN_TRACE] Saltando al siguiente place (lugar inválido). Próximo índice: ${index}`);
                setTimeout(() => processNextPlace(), 0);
                return;
            }
            // console.log(`[WME_PLN_TRACE] Venue Old Model obtenido: ID ${venueFromOldModel.getID()}`);

            // 3. Salto temprano si el venue es inválido o no tiene nombre
            if (!venueFromOldModel || typeof venueFromOldModel !== 'object' || !venueFromOldModel.attributes || typeof nameValue !== 'string' || nameValue.trim() === '')
            {
                //console.warn(`[WME_PLN_TRACE] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
                updateScanProgressBar(index, places.length);
                index++;
             //   console.log(`[WME_PLN_TRACE] Saltando al siguiente place (sin nombre/inválido). Próximo índice: ${index}`);
                setTimeout(() => processNextPlace(), 0);
                return;
            }
            const originalName = originalNameFull; // Usa la variable ya limpia de emoticones
            const currentVenueId = venueFromOldModel.getID();
           // console.log(`[WME_PLN_TRACE] Nombre original: "${originalName}", ID: ${currentVenueId}`);            
            // 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (USANDO venueSDK si está disponible) ---
            //console.log(`[WME_PLN_TRACE] Obteniendo información del editor...`);
            let lastEditorInfoForLog = "Editor: Desconocido";            
            //let resolvedEditorName = "N/D";
          //  let wasEditedByMe = false;

            let currentLoggedInUserId = null;
            let currentLoggedInUserName = null;

            if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user)
            {
                if (typeof W.loginManager.user.id === 'number')
                {
                    currentLoggedInUserId = W.loginManager.user.id;
                }
                if (typeof W.loginManager.user.userName === 'string')
                {
                    currentLoggedInUserName = W.loginManager.user.userName;
                }
            }
           // console.log(`[WME_PLN_TRACE] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`);
            if (venueSDK && venueSDK.modificationData)
            {
                const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
                //console.log(`[WME_PLN_TRACE] Info editor desde SDK:`, updatedByDataFromSDK);
                if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '')
                {
                    lastEditorInfoForLog = `Editor (SDK): ${updatedByDataFromSDK}`;
                    resolvedEditorName = updatedByDataFromSDK;
                   /* if (currentLoggedInUserName && currentLoggedInUserName === updatedByDataFromSDK)                    
                        wasEditedByMe = true;  */                  
                }
                else if (typeof updatedByDataFromSDK === 'number')
                {
                    lastEditorInfoForLog = `Editor (SDK): ID ${updatedByDataFromSDK}`;
                    resolvedEditorName = `ID ${updatedByDataFromSDK}`;
                    lastEditorIdForComparison = updatedByDataFromSDK;
                    if (typeof W !== 'undefined' && W.model && W.model.users)
                    {
                        const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
                        if (userObjectW && userObjectW.userName)
                        {
                            lastEditorInfoForLog = `Editor (SDK ID ${updatedByDataFromSDK} -> W.model): ${userObjectW.userName}`;
                            resolvedEditorName = userObjectW.userName;
                        }
                        else if (userObjectW)
                        {
                            lastEditorInfoForLog = `Editor (SDK ID ${updatedByDataFromSDK} -> W.model): ID ${updatedByDataFromSDK} (sin userName en W.model)`;
                        }
                    }
                }
                else if (updatedByDataFromSDK === null)
                {
                    lastEditorInfoForLog = "Editor (SDK): N/D (updatedBy es null)";
                    resolvedEditorName = "N/D";
                }
                else
                {
                    lastEditorInfoForLog = `Editor (SDK): Valor inesperado para updatedBy ('${updatedByDataFromSDK}')`;
                            resolvedEditorName = "Inesperado (SDK)";
                }
            }
            else
            {
               // console.log(`[WME_PLN_TRACE] Fallback a W.model para info de editor.`);
                const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
                if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined)
                {
                    lastEditorIdForComparison = oldModelUpdatedBy;
                    resolvedEditorName = `ID ${oldModelUpdatedBy}`;
                    let usernameFromOldModel = `ID ${oldModelUpdatedBy}`;
                    if (typeof W !== 'undefined' && W.model && W.model.users)
                    {
                        const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);
                        if (userObjectW && userObjectW.userName)
                        {
                            usernameFromOldModel = userObjectW.userName;
                            resolvedEditorName = userObjectW.userName;
                        }
                        else if (userObjectW)
                        {
                            usernameFromOldModel = `ID ${oldModelUpdatedBy} (sin userName)`;
                        }
                    }
                    lastEditorInfoForLog = `Editor (W.model Fallback): ${usernameFromOldModel}`;
                }
                else
                {
                    lastEditorInfoForLog = "Editor (W.model Fallback): N/D";
                    resolvedEditorName = "N/D";
                }
            }
// console.log(`[WME_PLN_TRACE] Info editor final: ${lastEditorInfoForLog}, Editado por mi: ${wasEditedByMe}`);
            // ---- FIN INFO DEL EDITOR ----
            // 5. --- PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA ---
            //console.log(`[WME_PLN_TRACE] Iniciando procesamiento palabra por palabra del nombre...`);
            let nombreSugeridoParcial = [];
            let sugerenciasLugar = {};
            const originalWords = originalName.split(/\s+/);
            const processingStepLabel = document.getElementById("processingStep");
            if (index === 0)
                sugerenciasPorPalabra = {};
            const newRomanBaseRegexString = "((XC|XL|L?X{0,3})(IX|IV|V?I{0,3})?|(IX|IV|V?I{0,3}))";
            const romanRegexStrict = new RegExp(`^${newRomanBaseRegexString}$`); // Sin 'i' para prueba con toUpperCase()
            const romanRegexStrictInsensitive = new RegExp(`^${newRomanBaseRegexString}$`, 'i'); // Con 'i' para isPotentiallyRomanNumeral

            originalWords.forEach((P, idx_word) => {
             //   console.log(`[WME_PLN_TRACE_WORD] Procesando palabra #${idx_word + 1}: "${P}"`);
                const endsWithComma = restoreCommas && P.endsWith(",");
                const baseWord = endsWithComma ? P.slice(0, -1) : P;
                const cleaned = baseWord.trim();
 //console.log(`[DEBUG WORD] Procesando "${P}". Cleaned: "${cleaned}"`); 
                // Si cleaned es una cadena vacía, no procesar más
                if (cleaned === "")
                {
                    nombreSugeridoParcial.push(cleaned);
//console.log(`[WME_PLN_TRACE_WORD] Palabra vacía, continuando.`);
                    return;
                }

                let isExcluded = false;
                let matchingExcludedWord = null;
                if (checkExcludedWords) {
                    matchingExcludedWord = excludedArray.find(w_excluded => removeDiacritics(w_excluded.toLowerCase()) === removeDiacritics(cleaned.toLowerCase()));
                    isExcluded = !!matchingExcludedWord;
                   // console.log(`[WME_PLN_TRACE_WORD] Excluida: ${isExcluded}${isExcluded ? ` (coincide con: "${matchingExcludedWord}")` : ''}`);
                }

                let tempReplaced;
                const isCommon = commonWords.includes(cleaned.toLowerCase());
                const isPotentiallyRomanNumeral = romanRegexStrictInsensitive.test(cleaned);
//console.log(`[WME_PLN_TRACE_WORD] Común: ${isCommon}, Potencial Romano: ${isPotentiallyRomanNumeral}`);

                if (isExcluded)
                {
                    tempReplaced = matchingExcludedWord;
                    if (romanRegexStrictInsensitive.test(tempReplaced))                    
                        tempReplaced = tempReplaced.toUpperCase();
 //console.log(`[DEBUG WORD] Es excluida. tempReplaced: "${tempReplaced}"`); 
                }
                else
                {
                    let dictionaryFormToUse = null;
                    let foundInDictionary = false;

                    if (checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function")
                    {
                        const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase());
                        const cleanedHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(cleaned);

                        for (const diccWord of window.dictionaryWords)
                        {
                            if (removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics)
                            {
                                foundInDictionary = true;
                                const diccWordHasDiacritics = /[áéíóúÁÉÍÓÚñÑ]/.test(diccWord);

                                if (cleanedHasDiacritics && !diccWordHasDiacritics)
                                {
                                    dictionaryFormToUse = cleaned;
                                }
                                else
                                {
                                    if (isPotentiallyRomanNumeral)
                                    {
                                        if (diccWord === diccWord.toUpperCase() && romanRegexStrict.test(diccWord))
                                        {
                                            dictionaryFormToUse = diccWord;
                                        }
                                    }
                                    else
                                    {
                                        dictionaryFormToUse = diccWord;
                                    }
                                }
                                break;
                            }
                        }
//console.log(`[WME_PLN_TRACE_WORD] Encontrada en diccionario: ${foundInDictionary}${dictionaryFormToUse ? ` (usando forma: "${dictionaryFormToUse}")` : ''}`);
                    }
                    //Verificar si se encontró una forma en el diccionario
                    if (dictionaryFormToUse !== null)
                    {
                        tempReplaced = dictionaryFormToUse;
 //console.log(`[DEBUG WORD] En diccionario. tempReplaced: "${tempReplaced}"`); 
 
                    }
                    else
                    {
                        tempReplaced = normalizePlaceName(cleaned);
 //console.log(`[DEBUG WORD] Normalizada por normalizePlaceName. tempReplaced: "${tempReplaced}"`); 
 
                    }
                    // Esta lógica capitaliza según si es romano, primera palabra, común, etc.
                    // Necesitamos asegurarnos que "Mi" y "Di" no se conviertan a "MI"/"DI" .
                    if (tempReplaced.toUpperCase() === "MI" || tempReplaced.toUpperCase() === "DI" || tempReplaced.toUpperCase() === "SI")
                    {
                        tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase();
                    }
                    else if (isPotentiallyRomanNumeral)
                    { // No es "MI" ni "DI", pero sí un romano potencial
                        const upperVersion = tempReplaced.toUpperCase();
                        if (romanRegexStrict.test(upperVersion))
                        {
                           tempReplaced = upperVersion;
                        }
                        else
                        { // No pasó la prueba estricta de romano, capitalizar normalmente
                           tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase();
                        }
                    }
                    else if (idx_word === 0)
                    { // No es "MI", "DI", ni romano, y es primera palabra
                        tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
                    }
                    else
                    { // No es "MI", "DI", ni romano, y no es primera palabra
                        if (isCommon)
                        {
                            tempReplaced = tempReplaced.toLowerCase();
                        }
                        else
                        {
                            tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
                        }
                    }
//console.log(`[DEBUG WORD] Después de capitalización final. tempReplaced: "${tempReplaced}"`); 
    
                }
                //console.log(`[WME_PLN_TRACE_WORD] Palabra temporalmente reemplazada a: "${tempReplaced}"`);

                // Generación de Sugerencias Clickeables
                const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase());
                const tempReplacedLowerNoDiacritics = removeDiacritics(tempReplaced.toLowerCase());

                if (cleaned !== tempReplaced && (!commonWords.includes(cleaned.toLowerCase()) || cleaned.toLowerCase() !== tempReplaced.toLowerCase()) && cleanedLowerNoDiacritics !== tempReplacedLowerNoDiacritics)
                {
                    if (!sugerenciasLugar[baseWord])
                        sugerenciasLugar[baseWord] = [];
                    if (!sugerenciasLugar[baseWord].some(s => s.word === cleaned && s.fuente === 'original_preserved'))                    
                        sugerenciasLugar[baseWord].push({ word: cleaned, similarity: 0.99, fuente: 'original_preserved' });                    
                }
                // Verificar si la palabra es un número romano potencial
                if (!isExcluded && checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function")
                {
                    //const dictionaryArray = Array.from(window.dictionaryWords);
                    const similarDictionary = findSimilarWords(cleaned, window.dictionaryIndex, similarityThreshold);
                    // Filtrar palabras similares que no son idénticas
                    if (similarDictionary.length > 0)
                    {
                        if (!sugerenciasLugar[baseWord]) sugerenciasLugar[baseWord] = [];

                        similarDictionary.forEach(dictSuggestion => {
                            // Evitar sugerir la palabra misma o la ya normalizada si son idénticas
                            if (dictSuggestion.word.toLowerCase() !== cleaned.toLowerCase() && dictSuggestion.word.toLowerCase() !== tempReplaced.toLowerCase())
                            {
                                if (!sugerenciasLugar[baseWord].some(s => s.word === dictSuggestion.word && s.fuente === 'dictionary'))
                                {
                                    sugerenciasLugar[baseWord].push({ ...dictSuggestion, fuente: 'dictionary' });
                                }
                            }
                        });
                    }
                }
                // Verificar si la palabra es un número romano potencial
                if (checkExcludedWords)
                {
                    //const similarExcluded = findSimilarWords(cleaned, excludedArray, similarityThreshold).filter(s => s.similarity < 1);
                    const similarExcluded = findSimilarWords(cleaned, excludedWordsMap, similarityThreshold).filter(s => s.similarity < 1);
                    if (similarExcluded.length > 0)
                    {
                        if (!sugerenciasLugar[baseWord]) sugerenciasLugar[baseWord] = [];
                        similarExcluded.forEach(excludedSuggestion => {
                            if (!sugerenciasLugar[baseWord].some(s => s.word === excludedSuggestion.word && s.fuente === 'excluded'))
                            {
                                sugerenciasLugar[baseWord].push({...excludedSuggestion, fuente: 'excluded' });
                                //console.log(`[WME_PLN_TRACE_WORD] Añadida sugerencia 'excluded': "${excludedSuggestion.word}" para "${baseWord}"`);
                            }
                        });
                    }
                }
                // Verificar si la palabra es un número romano potencial
                if (endsWithComma && !tempReplaced.endsWith(","))                
                    tempReplaced += ",";                
                nombreSugeridoParcial.push(tempReplaced);
            });
            // ---- FIN PROCESAMIENTO PALABRA POR PALABRA ----
//console.log(`[DEBUG FINAL] Nombre parcial antes de unirse: [${nombreSugeridoParcial.map(w => `"${w}"`).join(', ')}]`); 
            // 7. --- COMPILACIÓN DE suggestedName ---           
            const joinedSuggested = nombreSugeridoParcial.join(' ');
            let processedName = joinedSuggested;
            // Si el nombre es igual al original, no aplicar más transformaciones
            if (applyGeneralReplacements)
            {
//console.log(`[DEBUG FINAL] Antes de aplicarReemplazosGenerales: "${processedName}"`); 
                processedName = aplicarReemplazosGenerales(processedName);
//console.log(`[DEBUG FINAL] Después de aplicarReemplazosGenerales: "${processedName}"`); 
            }
            // Aplicar reglas especiales al nombre procesado
            processedName = aplicarReglasEspecialesNombre(processedName);
//console.log(`[DEBUG FINAL] Después de aplicarReglasEspecialesNombre: "${processedName}"`);            
          
            // Post-procesamiento de comillas y paréntesis
            processedName = postProcessQuotesAndParentheses(processedName);

            // Reemplazos definidos por el usuario
            if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0)
            {
                // Aplicar reemplazos definidos por el usuario
//console.log(`[DEBUG FINAL] Antes de aplicarReemplazosDefinidos: "${processedName}"`); 
                processedName = aplicarReemplazosDefinidos(processedName, replacementWords);
//console.log(`[DEBUG FINAL] Después de aplicarReemplazosDefinidos: "${processedName}"`); 
            }
            // Aplicar movimiento de palabras al inicio (SWAP) ---
            processedName = applyWordsToStartMovement(processedName); 
           // console.log(`[WME_PLN_TRACE] Después de movimiento de palabras al inicio (SWAP): "${processedName}"`);

            let suggestedName = processedName.replace(/\s{2,}/g, ' ').trim();
           // console.log(`[WME_PLN_TRACE] Nombre sugerido después de trim/espacios múltiples: "${suggestedName}"`);

            if (suggestedName.endsWith('.'))
            {
                suggestedName = suggestedName.slice(0, -1);
               // console.log(`[WME_PLN_TRACE] Nombre sugerido después de quitar punto final: "${suggestedName}"`);
            }      
            
            // 6. --- LÓGICA DE SALTO (SKIP) CONSOLIDADA ---
            //console.log(`[WME_PLN_TRACE] Evaluando lógica de salto...`);
            const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
            let shouldSkipThisPlace = false;
            let skipReasonLog = "";

            // Nueva condición: Si el nombre original (con emoticones) es diferente del sugerido (sin emoticones), NO SALTAR.
            if (originalNameRaw !== suggestedName)
            { // Comparar el original con lo sugerido.
                shouldSkipThisPlace = false; // No saltar si hubo algún cambio, incluyendo eliminación de emoticones.
            }
            else
            {
                let tempOriginalNormalized = aplicarReemplazosGenerales(originalName.trim());
                tempOriginalNormalized = aplicarReglasEspecialesNombre(tempOriginalNormalized);
                tempOriginalNormalized = postProcessQuotesAndParentheses(tempOriginalNormalized);
                if (tempOriginalNormalized.endsWith('.'))
                {
                    tempOriginalNormalized = tempOriginalNormalized.slice(0, -1);
                }
                tempOriginalNormalized = tempOriginalNormalized.replace(/\s{2,}/g, ' ').trim();

                // REEMPLAZO DE BLOQUE DE CONDICIÓN SKIP NORMALIZED
                const nombre = tempOriginalNormalized;
                const normalizadoFinal = suggestedName;
                const nombreClean = nombre.trim();
                const normalizadoClean = normalizadoFinal.trim();

                if (nombreClean === normalizadoClean)
                {
//console.log("[DEBUG COMPARACIÓN] Detectados como iguales sin cambios:", nombreClean, "===", normalizadoClean);
                  shouldSkipThisPlace = true;
                  skipReasonLog = `[SKIP NORMALIZED]`;
                  // No return  porque estamos en un bloque, así que solo marcamos el skip
                }
            }
            // --- Salto temprano si se determinó omitir el lugar ---
            if (shouldSkipThisPlace)
            {
                //if (skipReasonLog) console.log(`[WME_PLN_TRACE] ${skipReasonLog} Descartado "${originalName}"`);
                const updateFrequency = 5; // Actualiza cada 5 lugares la barra de progreso
                if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length)
                {
                    updateScanProgressBar(index, places.length);
                }
                index++;
                setTimeout(() => processNextPlace(), 0); // Continúa con el siguiente lugar
                return; 
            }
            //console.log(`[WME_PLN_TRACE] Decisión de salto: ${shouldSkipThisPlace} (${skipReasonLog})`);
            // ---- FIN LÓGICA DE SALTO ---
            // 8. Registrar o no en la lista de inconsistentes
//   console.log(`[WME_PLN_TRACE] Registrando lugar con inconsistencias...`);
            // *** Si Llegamos Aquí, El Lugar No Se Salta Y Necesitamos Su Info Completa Para La Tabla ***
            if (processingStepLabel) {
                processingStepLabel.textContent = "Registrando lugar(es) con inconsistencias...";
            }
            // Lógica de Categorías (solo para lugares no saltados)
            // currentCategoryName, currentCategoryIcon, currentCategoryTitle, dynamicSuggestions, currentCategoryKey
            // Estas variables fueron declaradas al inicio de processNextPlace.
            const shouldRecommendCategories = document.getElementById("chk-recommend-categories")?.checked ?? true;
            try
            {
                const lang = getWazeLanguage();
                currentCategoryKey = getPlaceCategoryName(venueFromOldModel, venueSDK);
                const categoryDetails = getCategoryDetails(currentCategoryKey);
                currentCategoryIcon = categoryDetails.icon;
                currentCategoryTitle = categoryDetails.description;
                currentCategoryName = categoryDetails.description;

                if (shouldRecommendCategories)                
                    dynamicSuggestions = findCategoryForPlace(originalName);                
                else                
                    dynamicSuggestions = [];                
            }
            catch (e)
            {
                console.error("[WME PLN] Error procesando las categorías:", e);
                currentCategoryName = "Error";
                currentCategoryIcon = "❓";
                currentCategoryTitle = "Error al obtener categoría";
                dynamicSuggestions = [];
                currentCategoryKey = "UNKNOWN";
            }
            // --- Fin de la Lógica de Categorías ---
            let lastEditorIdForComparison = null; // Re-inicializar para este bloque
            if (venueSDK && venueSDK.modificationData) {
                const updatedByDataFromSDK = venueSDK.modificationData.updatedBy;
                if (typeof updatedByDataFromSDK === 'string' && updatedByDataFromSDK.trim() !== '') {
                    resolvedEditorName = updatedByDataFromSDK;
                } else if (typeof updatedByDataFromSDK === 'number') {
                    lastEditorIdForComparison = updatedByDataFromSDK;
                    resolvedEditorName = `ID ${updatedByDataFromSDK}`;
                    if (W && W.model && W.model.users) {
                        const userObjectW = W.model.users.getObjectById(updatedByDataFromSDK);
                        if (userObjectW && userObjectW.userName) {
                            resolvedEditorName = userObjectW.userName;
                        }
                    }
                }
            } else { // Fallback a W.model
                const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
                if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined) {
                    lastEditorIdForComparison = oldModelUpdatedBy;
                    resolvedEditorName = `ID ${oldModelUpdatedBy}`;
                    if (W && W.model && W.model.users) {
                        const userObjectW = W.model.users.getObjectById(oldModelUpdatedBy);
                        if (userObjectW && userObjectW.userName) {
                            resolvedEditorName = userObjectW.userName;
                        }
                    }
                }
            }

            // Obtener información de la ciudad (solo para lugares no saltados)
            try {
                cityInfo = await getPlaceCityInfo(venueFromOldModel, venueSDK);
            } catch (e) {
                console.error(`[WME_PLN_TRACE] Error al obtener información de la ciudad para el venue ID ${currentVenueId}:`, e);
            }
            // === FIN OBTENCIÓN DE DATOS ADICIONALES ===

            // 8. Agregar a inconsistentes
            inconsistents.push({
                id: currentVenueId,
                original: originalName,
                normalized: suggestedName,
                editor: resolvedEditorName, // Usamos el nombre del editor resuelto
                cityIcon: cityInfo.icon,
                cityTitle: cityInfo.title,
                hasCity: cityInfo.hasCity,
                venueSDKForRender: venueSDK,                    
                currentCategoryName: currentCategoryName,
                currentCategoryIcon: currentCategoryIcon,
                currentCategoryTitle: currentCategoryTitle,
                currentCategoryKey: currentCategoryKey,
                dynamicCategorySuggestions: dynamicSuggestions
            });
            sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;

            // 9. Finalizar procesamiento del 'place' actual y pasar al siguiente
            const updateFrequency = 5;
            if ((index + 1) % updateFrequency === 0 || (index + 1) === places.length) {
                updateScanProgressBar(index, places.length);
            }
            index++;
            setTimeout(() => processNextPlace(), 0);
        } // ---- FIN DE LA FUNCIÓN processNextPlace ----

       // console.log("[WME_PLN_TRACE] Iniciando primer processNextPlace...");
        try
        {
            setTimeout(() => { processNextPlace(); }, 10);
        }
        catch (error)
        {
            console.error("[WME_PLN_TRACE][ERROR_CRITICAL] Fallo al iniciar processNextPlace:", error, error.stack);

             const outputFallback = document.querySelector("#wme-place-inspector-output");
             if (outputFallback) {
                outputFallback.innerHTML = `<div style='color:red; padding:10px;'><b>Error Crítico:</b> El script de normalización encontró un problema grave y no pudo continuar. Revise la consola para más detalles (F12).<br>Detalles: ${error.message}</div>`;
             }
             const scanBtn = document.querySelector("button[type='button']"); // Asumiendo que es el botón de Start Scan
             if(scanBtn) {
                scanBtn.disabled = false;
                scanBtn.textContent = "Start Scan... (Error Previo)";
             }
             if (window.processingDotsInterval) {
                clearInterval(window.processingDotsInterval);
             }
        }// ---- FIN DE LA FUNCIÓN processNextPlace ----

        function reapplyExcludedWordsLogic(text, excludedWordsSet)
        {
            if (typeof text !== 'string' || !excludedWordsSet || excludedWordsSet.size === 0)
            {
                return text;
            }

            const wordsInText = text.split(/\s+/);
            const processedWordsArray = wordsInText.map(word => {
                if (word === "") return "";

                const wordWithoutDiacriticsLower = removeDiacritics(word.toLowerCase());

                // Encontrar la palabra excluida que coincida (insensible a may/min y diacríticos)
                const matchingExcludedWord = Array.from(excludedWordsSet).find(
                    w_excluded => removeDiacritics(w_excluded.toLowerCase()) === wordWithoutDiacriticsLower
                );

                if (matchingExcludedWord)
                {
                    // Si coincide, DEVOLVER LA FORMA EXACTA DE LA LISTA DE EXCLUIDAS
                    return matchingExcludedWord;
                }
                // Si no, devolver la palabra como estaba (ya normalizada por pasos previos)
                return word;
            });
            return processedWordsArray.join(' ');
        }// ---- FIN DE LA FUNCIÓN reapplyExcludedWordsLogic ----
        //Función para finalizar renderizado una vez completado el análisis
        function finalizeRender(inconsistents, placesArr)
        {   // Limpiar el mensaje de procesamiento y spinner al finalizar el análisis
            // Detener animación de puntos suspensivos si existe
            if (window.processingDotsInterval)
            {
                clearInterval(window.processingDotsInterval);
                window.processingDotsInterval = null;
            }
            // Refuerza el restablecimiento del botón de escaneo al entrar
            const scanBtn = document.querySelector("button[type='button']");
            if (scanBtn)
            {
                scanBtn.textContent = "Start Scan...";
                scanBtn.disabled = false;
                scanBtn.style.opacity = "1";
                scanBtn.style.cursor = "pointer";
            }
            // Verificar si el botón de escaneo existe
            const output = document.querySelector("#wme-place-inspector-output");
            if (!output)
            {
                console.error("❌ No se pudo montar el panel flotante. Revisar estructura del DOM.");
                alert("Hubo un problema al mostrar los resultados. Intenta recargar la página.");
                return;
            }
            // Esta llamada se hace ANTES de limpiar el output. El primer argumento es el estado, el segundo es el número de inconsistencias.
            createFloatingPanel("results", inconsistents.length);
            // Limpiar el mensaje de procesamiento y spinner
            if (output)
            {
                // output.innerHTML = ""; // Limpiar el mensaje de procesamiento y spinner // ESTA LÍNEA SE ELIMINA O COMENTA
                // Mostrar el panel flotante al terminar el procesamiento
                // createFloatingPanel(inconsistents.length); // ESTA LÍNEA SE ELIMINA O COMENTA, YA SE HIZO ARRIBA
            }
            // Limitar a 30 resultados y mostrar advertencia si excede
            const maxRenderLimit = 30;
            const totalInconsistentsOriginal = inconsistents.length; // Guardar el total original
            let isLimited = false; // Declarar e inicializar isLimited
            // Si hay más de 30 resultados, limitar a 30 y mostrar mensaje
            if (totalInconsistentsOriginal > maxRenderLimit)
            {
                inconsistents = inconsistents.slice(0, maxRenderLimit);
                isLimited = true; // Establecer isLimited a true si se aplica el límite
                // Mostrar mensaje de advertencia si se aplica el límite
                if (!sessionStorage.getItem("popupShown"))
                {
                    const modalLimit = document.createElement("div"); // Renombrado a modalLimit para claridad
                    modalLimit.style.position = "fixed";
                    modalLimit.style.top = "50%";
                    modalLimit.style.left = "50%";
                    modalLimit.style.transform = "translate(-50%, -50%)";
                    modalLimit.style.background = "#fff";
                    modalLimit.style.border = "1px solid #ccc";
                    modalLimit.style.padding = "20px";
                    modalLimit.style.zIndex = "10007"; // <<<<<<< Z-INDEX AUMENTADO
                    modalLimit.style.width = "400px";
                    modalLimit.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)";
                    modalLimit.style.borderRadius = "8px";
                    modalLimit.style.fontFamily = "sans-serif";
                    // Fondo suave azul y mejor presentación
                    modalLimit.style.backgroundColor = "#f0f8ff";
                    modalLimit.style.border = "1px solid #aad";
                    modalLimit.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)";
                    // --- Insertar ícono visual de información arriba del mensaje ---
                    const iconInfo = document.createElement("div"); // Renombrado
                    iconInfo.innerHTML = "ℹ️";
                    iconInfo.style.fontSize = "24px";
                    iconInfo.style.marginBottom = "10px";
                    modalLimit.appendChild(iconInfo);
                    // Contenedor del mensaje
                    const message = document.createElement("p");
                    message.innerHTML = `Se encontraron <strong>${
                      totalInconsistentsOriginal}</strong> lugares con nombres no normalizados.<br><br>Solo se mostrarán los primeros <strong>${
                      maxRenderLimit}</strong>.<br><br>Una vez corrijas estos, presiona nuevamente <strong>'Start Scan...'</strong> para continuar con el análisis del resto.`;
                    message.style.marginBottom = "20px";
                    modalLimit.appendChild(message);
                    // Botón de aceptar
                    const acceptBtn = document.createElement("button");
                    acceptBtn.textContent = "Aceptar";
                    acceptBtn.style.padding = "6px 12px";
                    acceptBtn.style.cursor = "pointer";
                    acceptBtn.style.backgroundColor = "#007bff";
                    acceptBtn.style.color = "#fff";
                    acceptBtn.style.border = "none";
                    acceptBtn.style.borderRadius = "4px";
                    acceptBtn.addEventListener("click", () => {sessionStorage.setItem("popupShown", "true");
                        modalLimit.remove();
                    });
                    modalLimit.appendChild(acceptBtn);
                    document.body.appendChild(modalLimit); // Se añade al body, así que el z-index debería funcionar globalmente
                }
            }
            // Mostrar contador de registros
            const resultsCounter = document.createElement("div");
            resultsCounter.style.fontSize = "13px";
            resultsCounter.style.color = "#555"; // Color base para el texto normal
            resultsCounter.style.marginBottom = "8px";
            resultsCounter.style.textAlign = "left";
            // Mostrar el número total de inconsistencias encontradas
            if (totalInconsistentsOriginal > 0)
            {
                if (isLimited)
                {
                    resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando las primeras <span style="color: #ff0000;"><b>${inconsistents.length}</b></span> (límite de ${maxRenderLimit} aplicado).`;
                }
                else
                {
                    resultsCounter.innerHTML = `<span style="color: #ff0000;"><b>${totalInconsistentsOriginal}</b> inconsistencias encontradas</span>. Mostrando <span style="color: #ff0000;"><b>${inconsistents.length}</b></span>.`;
                }
            }
            else
            {
                // No se añaden resultados a la tabla si no hay inconsistencias,
                // pero el mensaje de "Todos los nombres... están correctamente normalizados" se manejará más abajo.
            }
            if (output && totalInconsistentsOriginal > 0) // Solo añadir si se encontraron inconsistencias originalmente
            {
                 output.appendChild(resultsCounter);
            }

            // Si no hay inconsistencias, mostrar mensaje y salir (progreso visible)
            if (inconsistents.length === 0) // Esto ahora significa que o no había nada, o se limitó a 0 (aunque es improbable con el límite de 30)
            {
                // Si totalInconsistentsOriginal también es 0, entonces realmente no había nada.
                if (totalInconsistentsOriginal === 0)
                {
                    output.appendChild(document.createTextNode("Todos los nombres de lugares visibles están correctamente normalizados."));
                    // Mensaje visual de análisis finalizado sin inconsistencias
                    const checkIcon = document.createElement("div");
                    checkIcon.innerHTML = "✔ Análisis finalizado sin inconsistencias.";
                    checkIcon.style.marginTop = "10px";
                    checkIcon.style.fontSize = "14px";
                    checkIcon.style.color = "green";
                    output.appendChild(checkIcon);
                    // Mensaje visual adicional solicitado
                    const successMsg = document.createElement("div");
                        successMsg.textContent = "Todos los nombres están correctamente normalizados.";
                    successMsg.style.marginTop = "10px";
                    successMsg.style.fontSize = "14px";
                    successMsg.style.color = "green";
                    successMsg.style.fontWeight = "bold";
                    output.appendChild(successMsg);
                }
                // Con inconsistents.length === 0 PERO totalInconsistentsOriginal > 0,
                // significa que el límite fue tan bajo que no se muestra nada, lo cual no debería pasar con un límite de 30
                // a menos que el total original fuera menor que 30 y luego se filtraran todos por alguna razón.
                // En este caso, el contador ya habrá mostrado el mensaje adecuado.

                const existingOverlay = document.getElementById("scanSpinnerOverlay");
                if (existingOverlay)
                    existingOverlay.remove();
                // Actualizar barra de progreso 100%
                const progressBarInnerTab =
                  document.getElementById("progressBarInnerTab");
                const progressBarTextTab =
                  document.getElementById("progressBarTextTab");
                if (progressBarInnerTab && progressBarTextTab)
                {
                    progressBarInnerTab.style.width = "100%";
                    progressBarTextTab.textContent =
                      `Progreso: 100% (${placesArr.length}/${placesArr.length})`;
                }
                // Mensaje adicional en el tab principal (pestaña)
                const outputTab =
                  document.getElementById("wme-normalization-tab-output");
                if (outputTab)
                {
                    outputTab.innerHTML =
                      `✔ Todos los nombres están normalizados. Se analizaron ${
                        placesArr.length} lugares.`;
                    outputTab.style.color = "green";
                    outputTab.style.fontWeight = "bold";
                }
                // Restaurar el texto y estado del botón de escaneo
                const scanBtn = document.querySelector("button[type='button']");
                if (scanBtn)
                {
                    scanBtn.textContent = "Start Scan...";
                    scanBtn.disabled = false;
                    scanBtn.style.opacity = "1";
                    scanBtn.style.cursor = "pointer";
                    // Agregar check verde al lado del botón al finalizar sin
                    // errores
                    const iconCheck = document.createElement("span");
                    iconCheck.textContent = " ✔";
                    iconCheck.style.marginLeft = "8px";
                    iconCheck.style.color = "green";
                    scanBtn.appendChild(iconCheck);
                }
                return;
            }
            //Permite renderizar la tabla de resultados
            const table = document.createElement("table");
            table.style.width = "100%";
            table.style.borderCollapse = "collapse";
            table.style.fontSize = "12px";
            // Añadir clase para estilo de tabla
            const thead = document.createElement("thead");
            // Añadir cabecera de la tabla
            const headerRow = document.createElement("tr");
            [
                "Perma",
                "Tipo/Ciudad",            
                "Editor",
                "Nombre Actual",
                "Nombre Sugerido",
                "Sugerencias de reemplazo",
                "Categoría",
                "Categoría<br>Recomendada", 
                "Acción"
            ].forEach(header => {
                const th = document.createElement("th");
                th.innerHTML = header; 
                th.style.borderBottom = "1px solid #ccc";
                th.style.padding = "4px";
                th.style.textAlign = "center";
                if (header === "Icon" || header === "Tipo") th.style.width = "65px";
                // if (header === "Categoría<br>Recomendada") th.style.width = "180px"; // Opcional: ajustar ancho de columna
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);
            table.appendChild(thead);
            thead.style.position = "sticky";
            thead.style.top = "0";
            thead.style.background = "#f1f1f1";
            thead.style.zIndex = "10"; // z-index de la cabecera de la tabla
            headerRow.style.backgroundColor = "#003366";
            headerRow.style.color = "#ffffff";
            thead.appendChild(headerRow);
            table.appendChild(thead);

            // Añadir el cuerpo de la tabla
            const tbody = document.createElement("tbody");
            // En el render de cada fila:
            inconsistents.forEach(({ id, original, normalized, editor, cityIcon, cityTitle, hasCity, currentCategoryName, currentCategoryIcon, currentCategoryTitle, currentCategoryKey, dynamicCategorySuggestions, venueSDKForRender }, index) => { 
                // Actualizar barra de progreso visual EN EL TAB PRINCIPAL
                const progressPercent =
                  Math.floor(((index + 1) / inconsistents.length) * 100);
                // Actualiza barra de progreso en el tab principal
                const progressBarInnerTab =
                  document.getElementById("progressBarInnerTab");
                const progressBarTextTab =
                  document.getElementById("progressBarTextTab");
                if (progressBarInnerTab && progressBarTextTab)
                {
                    progressBarInnerTab.style.width = `${progressPercent}%`;
                    progressBarTextTab.textContent = `Progreso: ${
                      progressPercent}% (${index + 1}/${inconsistents.length})`;
                }
            const row = document.createElement("tr");

            const permalinkCell = document.createElement("td");
            const link = document.createElement("a");
            link.href = "#";
            // Reemplazado onclick por addEventListener para mejor
            // compatibilidad y centrado de mapa
            link.addEventListener("click", (e) => {
                e.preventDefault();
                const venue = W.model.venues.getObjectById(id);
                if (!venue)
                    return;

                // Centrar mapa y seleccionar el lugar
                const geometry = venue.getGeometry();
                if (geometry && geometry.getCentroid)
                {
                    const center = geometry.getCentroid();
                    W.map.setCenter(center, null, false, 0);
                }

                if (W.selectionManager &&
                    typeof W.selectionManager.select === "function")
                {
                    W.selectionManager.select(venue);
                }
                else if (W.selectionManager &&
                            typeof W.selectionManager.setSelectedModels ===
                            "function")
                {
                    W.selectionManager.setSelectedModels([ venue ]);
                }
            });
            link.title = "Abrir en panel lateral";
            link.textContent = "🔗";
            permalinkCell.appendChild(link);
            permalinkCell.style.padding = "4px";
            permalinkCell.style.textAlign = "center"; // Centrar el ícono
            permalinkCell.style.width = "65px";
            row.appendChild(permalinkCell);
            // Combinada: Tipo / Ciudad ---
            const typeCityCell = document.createElement("td");
            // Obtener el objeto del venue por ID
            const venueObject = W.model.venues.getObjectById(id);
            const typeInfo = getPlaceTypeInfo(venueObject); // Obtenemos el ícono de Tipo (Punto/Área)            
            // Lógica condicional :
            if (hasCity)
            {
                // Si SÍ tiene ciudad, solo muestra el ícono de Tipo.
                typeCityCell.textContent = typeInfo.icon;
                typeCityCell.title = `Tipo: ${typeInfo.title} | Ciudad: ${cityTitle}`;
            }
            else
            {
                // Si NO tiene ciudad, muestra "Tipo / Ciudad".
                typeCityCell.innerHTML = `${typeInfo.icon} / <span style="color:red;">${cityIcon}</span>`;
                typeCityCell.title = `Tipo: ${typeInfo.title} | ${cityTitle}`;
            }
            typeCityCell.style.textAlign = "center";
            row.appendChild(typeCityCell);
            // Columna Editor (username)
            const editorCell = document.createElement("td");
            editorCell.textContent =  editor || "Desconocido"; // Use the stored editor name
            editorCell.title = "Último editor";
            editorCell.style.padding = "4px";
            editorCell.style.width = "140px";
            row.appendChild(editorCell);
                const originalCell = document.createElement("td");
                const inputOriginal = document.createElement("input");
                inputOriginal.type = "text";
                const venueLive = W.model.venues.getObjectById(id);
                const currentLiveName = venueLive?.attributes?.name?.value ||
                                        venueLive?.attributes?.name || "";
                inputOriginal.value = currentLiveName;
                // --- Resaltar en rojo si hay diferencia con el sugerido ---
                if (currentLiveName.trim().toLowerCase() !==
                    normalized.trim().toLowerCase())
                {
                    inputOriginal.style.border = "1px solid red";
                    inputOriginal.title =
                      "Este nombre difiere del original mostrado en el panel";
                }
                inputOriginal.disabled = true;
                inputOriginal.style.width = "270px";
                inputOriginal.style.backgroundColor = "#eee";
                originalCell.appendChild(inputOriginal);
                originalCell.style.padding = "4px";
                originalCell.style.width = "270px";
                row.appendChild(originalCell);
                const suggestionCell = document.createElement("td");
                
//------------------------------------------------------------------------------------------------------
                //sugerencia de reemplazo seleccionada
                const suggestionListCell = document.createElement("td");
                suggestionListCell.style.padding = "4px";
                suggestionListCell.style.fontSize = "11px";
                suggestionListCell.style.color = "#333";
                suggestionListCell.style.whiteSpace = "pre-wrap";
                suggestionListCell.style.wordBreak = "break-word";
                suggestionListCell.style.width = "270px";

                const allSuggestions = sugerenciasPorPalabra?.[id] || {};

                // --- Renderizar input principal de sugerencia ---
                const inputReplacement = document.createElement("input");
                inputReplacement.type = "text";
                inputReplacement.value = normalized; // Usar el valor normalizado final
                inputReplacement.style.width = "270px";
                inputReplacement.title = "Nombre normalizado";
                suggestionCell.appendChild(inputReplacement);
                suggestionCell.style.padding = "4px";
                suggestionCell.style.width = "270px";
                // --- INICIO: Lógica de Pistas Visuales (Colores de fondo)  ---
                let autoApplied = false;
                if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded' && s.similarity === 1)) {
                    autoApplied = true;
                }
                if (autoApplied) {
                    inputReplacement.style.backgroundColor = "#c8e6c9"; // verde claro
                    inputReplacement.title = "Reemplazo automático aplicado (palabra especial con 100% similitud)";
                } else if (Object.values(allSuggestions).flat().some(s => s.fuente === 'excluded')) {
                    inputReplacement.style.backgroundColor = "#fff3cd"; // amarillo claro
                    inputReplacement.title = "Contiene palabra especial reemplazada";
                }
                // --- FIN: Lógica de Pistas Visuales  ---

                // --- Función debounce (Conservada de tu código original) ---
                function debounce(func, delay) {
                    let timeout;
                    return function(...args) {
                        clearTimeout(timeout);
                        timeout = setTimeout(() => func.apply(this, args), delay);
                    };
                }
                // --- Activar/desactivar el botón Aplicar (Conservado de tu código original) ---
                inputReplacement.addEventListener('input', debounce(() => {
                    if (inputReplacement.value.trim() !== original) {
                        applyButton.disabled = false;
                        applyButton.style.color = "";
                    } else {
                        applyButton.disabled = true;
                        applyButton.style.color = "#bbb";
                    }
                }, 300));
                // --- Listener para inputOriginal (Conservado de tu código original) ---
                inputOriginal.addEventListener('input', debounce(() => {
                    // Opcional: alguna lógica si se desea manejar cambios en inputOriginal
                }, 300));
                // --- Lógica Unificada Para Renderizar Todas Las Sugerencias ---
                const suggestionContainer = document.createElement('div');
                const palabrasYaProcesadas = new Set();

                Object.entries(allSuggestions).forEach(([originalWord, suggestions]) => {
                    suggestions.forEach(s => {
                        let icono = '';
                        let textoSugerencia = '';
                        let colorFondo = '#f9f9f9';
                        let esSugerenciaValida = false;
                        let palabraAReemplazar = originalWord;
                        let palabraAInsertar = s.word;

                        switch (s.fuente) {
                            case 'original_preserved':
                                esSugerenciaValida = true;
                                icono = '⚙️';
                                textoSugerencia = `¿"${originalWord}" por "${s.word}"?`;
                                colorFondo = '#f0f0f0';
                                palabraAReemplazar = normalizePlaceName(originalWord);
                                palabraAInsertar = normalizePlaceName(s.word);
                                break;

                            case 'excluded':
                                if (s.similarity < 1 || (s.similarity === 1 && originalWord !== s.word)) {
                                    esSugerenciaValida = true;
                                    icono = '🏷️';
                                    textoSugerencia = `¿"${originalWord}" por "${s.word}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`;
                                    colorFondo = '#f3f9ff';
                                    palabraAReemplazar = normalizePlaceName(originalWord);
                                    palabraAInsertar = normalizePlaceName(s.word);
                                    palabrasYaProcesadas.add(originalWord.toLowerCase());
                                }
                                break;

                            case 'dictionary':
                                if (palabrasYaProcesadas.has(originalWord.toLowerCase())) break;

                                const normOriginal = normalizePlaceName(originalWord);
                                const normSugerida = normalizePlaceName(s.word);

                                if (normOriginal !== normSugerida) {
                                    esSugerenciaValida = true;
                                    icono = '📘';
                                    textoSugerencia = `¿"${normOriginal}" por "${normSugerida}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`;
                                    palabraAReemplazar = normOriginal;
                                    palabraAInsertar = normSugerida;
                                }
                                break;
                        }

                        if (esSugerenciaValida) {
                            const suggestionDiv = document.createElement("div");
                            suggestionDiv.innerHTML = `${icono} ${textoSugerencia}`;
                            suggestionDiv.style.cursor = "pointer";
                            suggestionDiv.style.padding = "2px 4px";
                            suggestionDiv.style.margin = "2px 0";
                            suggestionDiv.style.border = "1px solid #ddd";
                            suggestionDiv.style.borderRadius = "3px";
                            suggestionDiv.style.backgroundColor = colorFondo;

                            suggestionDiv.addEventListener("click", () => {
                                const currentSuggestedValue = inputReplacement.value;
                                const searchRegex = new RegExp("\\b" + escapeRegExp(palabraAReemplazar) + "\\b", "gi");
                                const newSuggestedValue = currentSuggestedValue.replace(searchRegex, palabraAInsertar);

                                if (inputReplacement.value !== newSuggestedValue) {
                                    inputReplacement.value = newSuggestedValue;
                                }
                                inputReplacement.dispatchEvent(new Event('input'));
                            });
                            suggestionContainer.appendChild(suggestionDiv);
                        }
                    });
                });
                suggestionListCell.appendChild(suggestionContainer);
                // Se añaden las celdas a la fila
                row.appendChild(suggestionCell);
                row.appendChild(suggestionListCell);
//---------------------------------------------------------------------------------------------------------------                
                
                // --- Columna Categoría (nombre y luego ícono abajo) ---
                const categoryCell = document.createElement("td");
                categoryCell.style.padding = "4px";
                categoryCell.style.width = "130px";
                categoryCell.style.textAlign = "center"; // Centra el contenido en la celda

                const currentCategoryDiv = document.createElement("div"); // Contenedor para el nombre y el ícono
                currentCategoryDiv.style.display = "flex";
                currentCategoryDiv.style.flexDirection = "column"; // Elementos apilados verticalmente
                currentCategoryDiv.style.alignItems = "center";     // Centrar horizontalmente
                currentCategoryDiv.style.gap = "2px"; // Pequeño espacio entre el nombre y el ícono

                const currentCategoryText = document.createElement("span");
                currentCategoryText.textContent = currentCategoryTitle;
                currentCategoryText.title = `Categoría Actual: ${currentCategoryTitle}`;
                currentCategoryDiv.appendChild(currentCategoryText);

                const currentCategoryIconDisplay = document.createElement("span");
                currentCategoryIconDisplay.textContent = currentCategoryIcon;
                currentCategoryIconDisplay.style.fontSize = "20px"; // Tamaño del ícono
                currentCategoryDiv.appendChild(currentCategoryIconDisplay);
                // Añadir el contenedor de categoría al cell
                categoryCell.appendChild(currentCategoryDiv);
                row.appendChild(categoryCell);
                // --- Columna Categoría Recomendada (nueva lógica de display) ---
            const recommendedCategoryCell = document.createElement("td");
            recommendedCategoryCell.style.padding = "4px";
            recommendedCategoryCell.style.width = "180px";
            recommendedCategoryCell.style.textAlign = "left";
            // Crear un contenedor para todas las entradas de categoría recomendada
            const allCategoriesContainer = document.createElement("div"); // Contenedor para todas las entradas de categoría
            allCategoriesContainer.style.display = "flex";
            allCategoriesContainer.style.flexDirection = "column";
            allCategoriesContainer.style.gap = "4px";
            // Lógica para determinar si hay categorías para recomendar (diferentes a la actual)
            const hasDifferentSuggestions = dynamicCategorySuggestions && dynamicCategorySuggestions.some(
                suggestion => suggestion.categoryKey.toUpperCase() !== currentCategoryKey.toUpperCase()
            );

                if (hasDifferentSuggestions)
                {
                // Si hay sugerencias diferentes, mostrar el ícono de categoría actual
                const suggestionsWrapper = document.createElement("div");
                suggestionsWrapper.style.display = "flex";
                suggestionsWrapper.style.flexDirection = "column";
                suggestionsWrapper.style.alignItems = "flex-start";
                suggestionsWrapper.style.marginTop = "8px"; // Espacio entre actual y sugerencias
                suggestionsWrapper.style.gap = "4px";

                // Itera sobre todas las categorías sugeridas
                dynamicCategorySuggestions.forEach(suggestion => {
                    if (suggestion.categoryKey.toUpperCase() !== currentCategoryKey.toUpperCase()) {
                        const suggestionEntry = document.createElement("div");
                        suggestionEntry.style.display = "flex";
                        suggestionEntry.style.alignItems = "center";
                        suggestionEntry.style.gap = "4px";

                        const suggestedIconSpan = document.createElement("span");
                        const suggestedDesc = (getWazeLanguage() === 'es' && suggestion.desc_es) ? suggestion.desc_es : suggestion.desc_en;
                        suggestedIconSpan.title = `Sugerencia: ${suggestedDesc}`;
                        suggestedIconSpan.style.cursor = "pointer";
                        suggestedIconSpan.style.border = "1px solid black";
                        suggestedIconSpan.style.borderRadius = "4px";
                        suggestedIconSpan.style.padding = "0 2px";
                        suggestedIconSpan.style.fontSize = "20px";
                        suggestedIconSpan.textContent = suggestion.icon;
                        suggestedIconSpan.dataset.categoryKey = suggestion.categoryKey;

                        suggestionEntry.appendChild(suggestedIconSpan);
                        suggestionEntry.appendChild(document.createTextNode(suggestedDesc));

                        suggestedIconSpan.addEventListener("click", async function() {
                            const placeToUpdate = W.model.venues.getObjectById(id);
                            if (placeToUpdate) {
                                const UpdateObject = require("Waze/Action/UpdateObject");
                                const action = new UpdateObject(placeToUpdate, { categories: [this.dataset.categoryKey] });
                                W.model.actionManager.add(action);

                                const allSuggestionsForThisPlace = suggestionsWrapper.querySelectorAll('span[data-category-key]');
                                allSuggestionsForThisPlace.forEach(sugSpan => {
                                    sugSpan.style.cursor = 'not-allowed';
                                    sugSpan.style.border = '1px solid #ccc';
                                    sugSpan.style.opacity = '0.6';
                                    sugSpan.removeEventListener('click', arguments.callee);
                                });

                                this.style.border = '1px solid green';
                                this.style.backgroundColor = '#e6ffe6';
                                this.style.cursor = 'default';

                                row.style.backgroundColor = "#d4edda";
                            }
                        });
                        suggestionsWrapper.appendChild(suggestionEntry);
                    }
                });
                allCategoriesContainer.appendChild(suggestionsWrapper);
                recommendedCategoryCell.appendChild(allCategoriesContainer);
            }
            // Si no hay hasDifferentSuggestions, recommendedCategoryCell permanecerá vacía
            row.appendChild(recommendedCategoryCell);


//---------------------------------------------------------------------------------------------------------------                
                // --- Columna Acción ---
                const actionCell = document.createElement("td");
                actionCell.style.padding = "4px";
                actionCell.style.width = "120px";
                // Crear botones de acción
                const buttonGroup = document.createElement("div");
                buttonGroup.style.display = "flex";
                buttonGroup.style.gap = "4px";
                // Botón de aplicar sugerencia
                const applyButton = document.createElement("button");
                applyButton.textContent = "✔";
                applyButton.title = "Aplicar sugerencia";
                applyButton.style.padding = "4px 8px";
                applyButton.style.cursor = "pointer";
                // Deshabilitar botón si no hay cambios
                const deleteButton = document.createElement("button");
                deleteButton.textContent = "💣";
                deleteButton.title = "Eliminar lugar";
                deleteButton.style.padding = "4px 8px";
                deleteButton.style.cursor = "pointer";
                applyButton.relatedDelete = deleteButton;
                deleteButton.relatedApply = applyButton;
                // Listener para el botón de aplicar
                applyButton.addEventListener("click", () => {
                    const venue = W.model.venues.getObjectById(id);
                    if (!venue)
                    {
                        alert(
                          "Error: El lugar no está disponible o ya fue eliminado.");
                        return;
                    }
                    const newName = inputReplacement.value.trim();
                    try
                    {
                        const UpdateObject = require("Waze/Action/UpdateObject");
                        const action = new UpdateObject(venue, { name : newName });
                        W.model.actionManager.add(action);
                        applyButton.disabled = true;
                        applyButton.style.color = "#bbb";
                        applyButton.style.opacity = "0.5";
                        if (applyButton.relatedDelete)
                        {
                            applyButton.relatedDelete.disabled = true;
                            applyButton.relatedDelete.style.color = "#bbb";
                            applyButton.relatedDelete.style.opacity = "0.5";
                        }
                        const successIcon = document.createElement("span");
                        successIcon.textContent = " ✅";
                        successIcon.style.marginLeft = "5px";
                        applyButton.parentElement.appendChild(successIcon);
                    }
                    catch (e)
                    {
                        alert("Error al actualizar: " + e.message);
                    }
                });
                // Listener para el botón de eliminar
                deleteButton.addEventListener("click", () => {
                // Modal bonito de confirmación
                const confirmModal = document.createElement("div");
                confirmModal.style.position = "fixed";
                confirmModal.style.top = "50%";
                confirmModal.style.left = "50%";
                confirmModal.style.transform = "translate(-50%, -50%)";
                confirmModal.style.background = "#fff";
                confirmModal.style.border = "1px solid #aad";
                confirmModal.style.padding = "28px 32px 20px 32px";
                confirmModal.style.zIndex = "20000"; // Z-INDEX AUMENTADO
                confirmModal.style.boxShadow = "0 4px 24px rgba(0,0,0,0.18)";
                confirmModal.style.fontFamily = "sans-serif";
                confirmModal.style.borderRadius = "10px";
                confirmModal.style.textAlign = "center";
                confirmModal.style.minWidth = "340px";

                // Ícono visual
                const iconElement = document.createElement("div");
                iconElement.innerHTML = "⚠️";
                iconElement.style.fontSize = "38px";
                iconElement.style.marginBottom = "10px";
                confirmModal.appendChild(iconElement);

                // Mensaje principal
                const message = document.createElement("div");
                const venue = W.model.venues.getObjectById(id);
                const placeName = venue?.attributes?.name?.value || venue?.attributes?.name || "este lugar";
                message.innerHTML = `<b>¿Eliminar "${placeName}"?</b>`; // CORREGIDO para mostrar el nombre del lugar
                message.style.fontSize = "18px";
                message.style.marginBottom = "8px";
                confirmModal.appendChild(message);

                // Nombre del lugar (puede ser redundante si ya está en el mensaje, pero se mantiene por si acaso)
                const nameDiv = document.createElement("div");
                nameDiv.textContent = `"${placeName}"`;
                nameDiv.style.fontSize = "15px";
                nameDiv.style.color = "#007bff";
                nameDiv.style.marginBottom = "18px";
                confirmModal.appendChild(nameDiv);

                // Botones
                const buttonWrapper = document.createElement("div");
                buttonWrapper.style.display = "flex";
                buttonWrapper.style.justifyContent = "center";
                buttonWrapper.style.gap = "18px";

                const cancelBtn = document.createElement("button");
                cancelBtn.textContent = "Cancelar";
                cancelBtn.style.padding = "7px 18px";
                cancelBtn.style.background = "#eee";
                cancelBtn.style.border = "none";
                cancelBtn.style.borderRadius = "4px";
                cancelBtn.style.cursor = "pointer";
                cancelBtn.addEventListener("click", () => confirmModal.remove());

                const confirmBtn = document.createElement("button");
                confirmBtn.textContent = "Eliminar";
                confirmBtn.style.padding = "7px 18px";
                confirmBtn.style.background = "#d9534f";
                confirmBtn.style.color = "#fff";
                confirmBtn.style.border = "none";
                confirmBtn.style.borderRadius = "4px";
                confirmBtn.style.cursor = "pointer";
                confirmBtn.style.fontWeight = "bold";
                confirmBtn.addEventListener("click", () => {
                    const venue = W.model.venues.getObjectById(id);
                    if (!venue) {
                        alert("El lugar no está disponible o ya fue eliminado.");
                        confirmModal.remove();
                        return;
                    }
                    try {
                        const DeleteObject = require("Waze/Action/DeleteObject");
                        const action = new DeleteObject(venue);
                        W.model.actionManager.add(action);
                        deleteButton.disabled = true;
                        deleteButton.style.color = "#bbb";
                        deleteButton.style.opacity = "0.5";
                        if (deleteButton.relatedApply) {
                            deleteButton.relatedApply.disabled = true;
                            deleteButton.relatedApply.style.color = "#bbb";
                            deleteButton.relatedApply.style.opacity = "0.5";
                        }
                        const successIcon = document.createElement("span");
                        successIcon.textContent = " 🗑️";
                        successIcon.style.marginLeft = "5px";
                        deleteButton.parentElement.appendChild(successIcon);
                    } catch (e) {
                        alert("Error al eliminar: " + e.message);
                    }
                    confirmModal.remove();
                });

                buttonWrapper.appendChild(cancelBtn);
                buttonWrapper.appendChild(confirmBtn);
                confirmModal.appendChild(buttonWrapper);

                document.body.appendChild(confirmModal);
            });

                buttonGroup.appendChild(applyButton);
                buttonGroup.appendChild(deleteButton);

                const addToExclusionBtn = document.createElement("button");
                addToExclusionBtn.textContent = "🏷️";
                addToExclusionBtn.title =
                  "Marcar palabra como especial (no se modifica)";
                addToExclusionBtn.style.padding = "4px 6px";
                addToExclusionBtn.addEventListener("click", () => {
                    const words = original.split(/\s+/);
                    const modal = document.createElement("div");
                    modal.style.position = "fixed";
                    modal.style.top = "50%";
                    modal.style.left = "50%";
                    modal.style.transform = "translate(-50%, -50%)";
                    modal.style.background = "#fff";
                    modal.style.border = "1px solid #ccc";
                    modal.style.padding = "10px";
                    modal.style.zIndex = "20000"; // Z-INDEX AUMENTADO
                    modal.style.maxWidth = "300px";

                    const title = document.createElement("h4");
                    title.textContent = "Agregar palabra a especiales";
                    modal.appendChild(title);

                    const list = document.createElement("ul");
                    list.style.listStyle = "none";
                    list.style.padding = "0";
                    words.forEach(w => {
                        // --- Filtro: palabras vacías, comunes, o ya existentes
                        // (ignorar mayúsculas) ---
                        if (w.trim() === '')
                            return;
                        const lowerW = w.trim().toLowerCase();
                       // evitar caracteres especiales solos
                        if(!/[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9]/.test(lowerW) ||  // No contiene letras ni números
                            /^[^a-zA-Z0-9]+$/.test(lowerW)  )              // Solo tiene caracteres especiales
                            return;
                        const alreadyExists =
                          Array.from(excludedWords)
                            .some(existing => existing.toLowerCase() === lowerW);
                        if (commonWords.includes(lowerW) || alreadyExists)
                            return;
                        const li = document.createElement("li");
                        const checkbox = document.createElement("input");
                        checkbox.type = "checkbox";
                        checkbox.value = w;
                        checkbox.id = `cb-exc-${w.replace(/[^a-zA-Z0-9]/g, "")}`;
                        li.appendChild(checkbox);
                        const label = document.createElement("label");
                        label.htmlFor = checkbox.id;
                        label.appendChild(document.createTextNode(" " + w));
                        li.appendChild(label);
                        list.appendChild(li);
                    });
                    modal.appendChild(list);

                    const confirmBtn = document.createElement("button");
                    confirmBtn.textContent = "Añadir Seleccionadas";
                    confirmBtn.addEventListener("click", () => {
                        const checked =  modal.querySelectorAll("input[type=checkbox]:checked");
                        let wordsActuallyAdded = false; // Para saber si se añadió algo nuevo

                        checked.forEach(c => {
                            if (!excludedWords.has(c.value))
                            {
                                excludedWords.add(c.value);
                                wordsActuallyAdded = true;
                            }
                        });

                        if (wordsActuallyAdded) {
                            // Llama a renderExcludedWordsList para actualizar la UI en la pestaña "Especiales"
                            // y para guardar en localStorage
                            if (typeof renderExcludedWordsList === 'function') {
                                // Es mejor pasar el elemento si se puede, o dejar que la función lo encuentre
                                const excludedListElement = document.getElementById("excludedWordsList");
                                if (excludedListElement) {
                                    renderExcludedWordsList(excludedListElement);
                                } else {
                                    renderExcludedWordsList(); // Fallback
                                }
                            }
                        }

                        modal.remove();
                    });
                    modal.appendChild(confirmBtn);

                    const cancelBtn = document.createElement("button");
                    cancelBtn.textContent = "Cancelar";
                    cancelBtn.style.marginLeft = "8px";
                    cancelBtn.addEventListener("click", () => modal.remove());
                    modal.appendChild(cancelBtn);

                    document.body.appendChild(modal);
                });
                buttonGroup.appendChild(addToExclusionBtn);
             //   buttonGroup.appendChild(addToDictionaryBtn);
                actionCell.appendChild(buttonGroup);
                row.appendChild(actionCell);
//-----------------------------------------------------------------------------------------------------------------------------------






                // Añadir borde inferior visible entre cada lugar
                row.style.borderBottom = "1px solid #ddd";
                row.style.backgroundColor = index % 2 === 0 ? "#f9f9f9" : "#ffffff";

                tbody.appendChild(row);
                // Actualizar progreso al final del ciclo usando setTimeout para
                // liberar el hilo visual
                setTimeout(() => {
                    const progress =
                      Math.floor(((index + 1) / inconsistents.length) * 100);
                    const progressElem =
                      document.getElementById("scanProgressText");
                    if (progressElem)
                    {
                        progressElem.textContent = `Analizando lugares: ${
                          progress}% (${index + 1}/${inconsistents.length})`;
                    }
                }, 0);
            });

            table.appendChild(tbody);
            output.appendChild(table);

            // Log de cierre
            //   console.log("✔ Panel finalizado y tabla renderizada.");

            // Quitar overlay spinner justo antes de mostrar la tabla
            const existingOverlay = document.getElementById("scanSpinnerOverlay");
            if (existingOverlay)
            {
                existingOverlay.remove();
            }
            // Al finalizar, actualizar el texto final en el tab principal (progreso
            // 100%)
            const progressBarInnerTab =
              document.getElementById("progressBarInnerTab");
            const progressBarTextTab =
              document.getElementById("progressBarTextTab");
            if (progressBarInnerTab && progressBarTextTab)
            {
                progressBarInnerTab.style.width = "100%";
                progressBarTextTab.textContent =
                  `Progreso: 100% (${inconsistents.length}/${placesArr.length})`;
            }
            // Función para reactivar todos los botones de acción en el panel
            // flotante
            function reactivateAllActionButtons()
            {
                document.querySelectorAll("#wme-place-inspector-output button")
                  .forEach(btn => {
                      btn.disabled = false;
                      btn.style.color = "";
                      btn.style.opacity = "";
                  });
            }

            W.model.actionManager.events.register("afterundoaction", null, () => {
                    // Verificar si el panel flotante está visible
                    if (floatingPanelElement && floatingPanelElement.style.display !== 'none') {
                    waitForWazeAPI(() => {
                        const places = getVisiblePlaces();
                            renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
                            setTimeout(reactivateAllActionButtons, 250);
                    });
                    } else {
                        console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
                        // Opcionalmente, solo resetear el estado del inspector si el panel no está visible
                        // resetInspectorState(); // Descomentar si se desea este comportamiento
                    }
            });
            W.model.actionManager.events.register("afterredoaction", null, () => {
                    // Verificar si el panel flotante está visible
                    if (floatingPanelElement && floatingPanelElement.style.display !== 'none') {
                    waitForWazeAPI(() => {
                        const places = getVisiblePlaces();
                            renderPlacesInFloatingPanel(places); // Esto mostrará el panel de "procesando" y luego resultados
                    setTimeout(reactivateAllActionButtons, 250);
                });
                    } else {
                        console.log("[WME PLN] Undo/Redo: Panel de resultados no visible, no se re-escanea.");
                        // Opcionalmente, solo resetear el estado del inspector si el panel no está visible
                        // resetInspectorState(); // Descomentar si se desea este comportamiento
                    }
            });
            // Mostrar el panel flotante al terminar el procesamiento
            // createFloatingPanel(inconsistents.length); // Ahora se invoca arriba
            // si output existe
        }
    }

    function getLevenshteinDistance(a, b)
    {
        const matrix = Array.from(
          { length : b.length + 1 },
          (_, i) => Array.from({ length : a.length + 1 },
                               (_, j) => (i === 0 ? j : (j === 0 ? i : 0))));

        for (let i = 1; i <= b.length; i++)
        {
            for (let j = 1; j <= a.length; j++)
            {
                if (b.charAt(i - 1) === a.charAt(j - 1))
                {
                    matrix[i][j] = matrix[i - 1][j - 1];
                }
                else
                {
                    matrix[i][j] = Math.min(
                      matrix[i - 1][j] + 1,    // deletion
                      matrix[i][j - 1] + 1,    // insertion
                      matrix[i - 1][j - 1] + 1 // substitution
                    );
                }
            }
        }

        return matrix[b.length][a.length];
    }

    function calculateSimilarity(word1, word2)
    {
        const distance =
          getLevenshteinDistance(word1.toLowerCase(), word2.toLowerCase());
        const maxLen = Math.max(word1.length, word2.length);
        return 1 - distance / maxLen;
    }
    function findSimilarWords(word, indexedListOrArray, threshold)
    {
        const lowerWord = word.toLowerCase();
        const firstChar = lowerWord.charAt(0);

        let candidates = [];

        // === Lógica CLAVE para usar el índice ===
        // Si el segundo argumento es un Map (como excludedWordsMap)
        if (indexedListOrArray instanceof Map) {
            candidates = Array.from(indexedListOrArray.get(firstChar) || []);
        }
        // Si el segundo argumento es un objeto literal (como window.dictionaryIndex)
        else if (indexedListOrArray && typeof indexedListOrArray === 'object' && !Array.isArray(indexedListOrArray) && indexedListOrArray[firstChar]) {
            candidates = Array.from(indexedListOrArray[firstChar] || []); // window.dictionaryIndex almacena arrays, asegúrate de que sean copiados o Set a Array.from
        }
        // Si es un Set o Array (menos óptimo, pero fallback)
        else if (indexedListOrArray instanceof Set || Array.isArray(indexedListOrArray)) {
            // Este es un fallback, filtra por primera letra aquí si no hay Map/objeto índice
            candidates = Array.from(indexedListOrArray).filter(candidate => candidate.charAt(0).toLowerCase() === firstChar);
        } else {
            return []; // No hay candidatos válidos para buscar
        }

        return candidates
            .map(candidate => {
                const similarity = calculateSimilarity(lowerWord, candidate.toLowerCase());
                return { word : candidate, similarity };
            })
            .filter(item => item.similarity >= threshold)
            .sort((a, b) => b.similarity - a.similarity);
    }

    function suggestExcludedReplacements(currentName, excludedWords)
    {
        const words = currentName.split(/\s+/);
        const suggestions = {};
        const threshold =
          parseFloat(document.getElementById("similarityThreshold")?.value ||
                     "85") /
          100;
        words.forEach(word => {
            const similar =
              findSimilarWords(word, Array.from(excludedWords), threshold);
            if (similar.length > 0)
            {
                suggestions[word] = similar;
            }
        });
        return suggestions;
    }

    // Reset del inspector: progreso y texto de tab
        function resetInspectorState()
        {
        const inner = document.getElementById("progressBarInnerTab");
        const text = document.getElementById("progressBarTextTab");
        const outputTab = document.getElementById("wme-normalization-tab-output");
        if (inner)
            inner.style.width = "0%";
        if (text)
            text.textContent = `Progreso: 0% (0/0)`;
        if (outputTab)
            outputTab.textContent = "Presiona 'Start Scan...' para analizar los lugares visibles.";
    }
    //Permite crear un panel flotante para mostrar los resultados del escaneo
    function createFloatingPanel(status = "processing", numInconsistents = 0)
    {
        if (!floatingPanelElement)
        {
            floatingPanelElement = document.createElement("div");
            floatingPanelElement.id = "wme-place-inspector-panel";
            floatingPanelElement.style.position = "fixed";
            floatingPanelElement.style.zIndex = "10005"; // Z-INDEX DEL PANEL DE RESULTADOS
            floatingPanelElement.style.background = "#fff";
            floatingPanelElement.style.border = "1px solid #ccc";
            floatingPanelElement.style.borderRadius = "8px";
            floatingPanelElement.style.boxShadow = "0 5px 15px rgba(0,0,0,0.2)";
            floatingPanelElement.style.padding = "10px";
            floatingPanelElement.style.fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
            floatingPanelElement.style.display = 'none';
            floatingPanelElement.style.transition = "width 0.25s, height 0.25s, left 0.25s, top 0.25s"; // Agregado left y top a la transición
            floatingPanelElement.style.overflow = "hidden"; 
            // Dimensiones del panel
            const closeBtn = document.createElement("span");
            closeBtn.textContent = "×";
            closeBtn.style.position = "absolute";
            closeBtn.style.top = "8px";
            closeBtn.style.right = "12px";
            closeBtn.style.cursor = "pointer";
            closeBtn.style.fontSize = "22px";
            closeBtn.style.color = "#555";
            closeBtn.title = "Cerrar panel";
            closeBtn.addEventListener("click", () => {
                if (floatingPanelElement) floatingPanelElement.style.display = 'none';
                     resetInspectorState();
            });
            floatingPanelElement.appendChild(closeBtn);
            // Dimensiones del panel de procesamiento
            const titleElement = document.createElement("h4");
            titleElement.id = "wme-pln-panel-title";
            titleElement.style.marginTop = "0";
            titleElement.style.marginBottom = "10px";
            titleElement.style.fontSize = "20px";
            titleElement.style.color = "#333";
            titleElement.style.textAlign = "center";
            titleElement.style.fontWeight = "bold";
            floatingPanelElement.appendChild(titleElement);
            // Dimensiones del panel de resultados
            const outputDivLocal = document.createElement("div");
            outputDivLocal.id = "wme-place-inspector-output";
            outputDivLocal.style.fontSize = "14px";
            outputDivLocal.style.backgroundColor = "#fdfdfd";
            outputDivLocal.style.overflowY = "auto"; 
            floatingPanelElement.appendChild(outputDivLocal);
            document.body.appendChild(floatingPanelElement);
        }
        // Dimensiones del panel de procesamiento
        const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title");
        // Dimensiones del panel de resultados
        const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");
        // Dimensiones del panel de procesamiento
        if(outputDiv) outputDiv.innerHTML = "";
        // Dimensiones del panel de procesamiento
        if (status === "processing")
        {
            floatingPanelElement.style.width = processingPanelDimensions.width;
            floatingPanelElement.style.height = processingPanelDimensions.height;
            if(outputDiv) outputDiv.style.height = "150px";
            if(titleElement) titleElement.textContent = "Buscando...";
            if (outputDiv)
            {
                 outputDiv.innerHTML = "<div style='display:flex; align-items:center; justify-content:center; height:100%;'><span class='loader-spinner' style='width:32px; height:32px; border:4px solid #ccc; border-top:4px solid #007bff; border-radius:50%; animation:spin 0.8s linear infinite;'></span></div>";
            }
            // Centrar el panel de procesamiento
            floatingPanelElement.style.top = "50%";
            floatingPanelElement.style.left = "50%";
            floatingPanelElement.style.transform = "translate(-50%, -50%)";
        }
        else
        { // status === "results"
            floatingPanelElement.style.width = resultsPanelDimensions.width;
            floatingPanelElement.style.height = resultsPanelDimensions.height;
            if(outputDiv) outputDiv.style.height = "660px";
            if(titleElement) titleElement.textContent = "Resultado de la búsqueda";
            // Mover el panel de resultados más a la derecha
            floatingPanelElement.style.top = "50%";
            floatingPanelElement.style.left = "60%";
            floatingPanelElement.style.transform = "translate(-50%, -50%)";
        }
        floatingPanelElement.style.display = 'flex';
        floatingPanelElement.style.flexDirection = 'column';
    }
    // Escuchar el botón Guardar de WME para resetear el inspector
    const wmeSaveBtn = document.querySelector(
    "button.action.save, button[title='Guardar'], button[aria-label='Guardar']");
    if (wmeSaveBtn)
    {
        wmeSaveBtn.addEventListener("click", () => resetInspectorState());
    }
    function createSidebarTab()
    {
        try
        {
            // 1. Verificar si WME y la función para registrar pestañas están listos
            if (!W || !W.userscripts ||
                typeof W.userscripts.registerSidebarTab !== 'function')
            {
                console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral.");
                return;
            }

            // 2. Registrar la pestaña principal del script en WME y obtener tabPane
            let registration;
            try
            {
                registration = W.userscripts.registerSidebarTab(
                "NrmliZer"); // Nombre del Tab que aparece en WME
            }
            catch (e)
            {
                if (e.message.includes("already been registered"))
                {
                    console.warn(
                    "[WME PLN] Tab 'NrmliZer' ya registrado. El script puede no funcionar como se espera si hay múltiples instancias.");
                    // Podrías intentar obtener el tabPane existente o simplemente
                    // retornar. Para evitar mayor complejidad, si ya está
                    // registrado, no continuaremos con la creación de la UI de la
                    // pestaña.
                    return;
                }
                //console.error("[WME PLN] Error registrando el sidebar tab:", e);
                throw e; // Relanzar otros errores para que se vean en consola
            }

            const { tabLabel, tabPane } = registration;
            if (!tabLabel || !tabPane)
            {
                //console.error("[WME PLN] Falló el registro del Tab: 'tabLabel' o 'tabPane' no fueron retornados.");
                return;
            }

            // Configurar el ícono y nombre de la pestaña principal del script
            tabLabel.innerHTML = `
            <img src="data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q=="
            style="height: 16px; vertical-align: middle; margin-right: 5px;">
            NrmliZer
        `;
            // 3. Inicializar las pestañas internas (General, Especiales,
            // Diccionario, Reemplazos)
            const tabsContainer = document.createElement("div");
            tabsContainer.style.display = "flex";
            tabsContainer.style.marginBottom = "8px";
            tabsContainer.style.gap = "8px";

            const tabButtons = {};
            const tabContents = {}; // Objeto para guardar los divs de contenido



            // Crear botones para cada pestaña
            tabNames.forEach(({ label, icon }) => {
                const btn = document.createElement("button");
                btn.innerHTML = icon
                ? `<span style="display: inline-flex; align-items: center; font-size: 11px;">
                    <span style="font-size: 12px; margin-right: 4px;">${icon}</span>${label}
                </span>`
                : `<span style="font-size: 11px;">${label}</span>`;



                btn.style.fontSize = "11px";
                btn.style.padding = "4px 8px";
                btn.style.marginRight = "4px";
                btn.style.minHeight = "28px";
                btn.style.border = "1px solid #ccc";
                btn.style.borderRadius = "4px 4px 0 0";
                btn.style.cursor = "pointer";
                btn.style.borderBottom = "none"; // Para que la pestaña activa se vea mejor integrada

                btn.className = "custom-tab-style";

                // Agrega el tooltip personalizado para cada tab
                if (label === "Gene") btn.title = "Configuración general";
                else if (label === "Espe") btn.title = "Palabras especiales (Excluidas)";
                else if (label === "Dicc") btn.title = "Diccionario de palabras válidas";
                else if (label === "Reemp") btn.title = "Gestión de reemplazos automáticos";

                // Estilo inicial: la primera pestaña es la activa
                if (label === tabNames[0].label) {
                    btn.style.backgroundColor = "#ffffff"; // Color de fondo activo (blanco)
                    btn.style.borderBottom = "2px solid #007bff"; // Borde inferior distintivo para la activa
                    btn.style.fontWeight = "bold";
                } else {
                    btn.style.backgroundColor = "#f0f0f0"; // Color de fondo inactivo (gris claro)
                    btn.style.fontWeight = "normal";
                }

                btn.addEventListener("click", () => {
                    tabNames.forEach(({label : tabLabel_inner}) => {
                        const isActive = (tabLabel_inner === label);
                        const currentButton = tabButtons[tabLabel_inner];

                        if (tabContents[tabLabel_inner]) {
                            tabContents[tabLabel_inner].style.display = isActive ? "block" : "none";
                        }

                        if (currentButton) {
                            // Aplicar/Quitar estilos de pestaña activa directamente
                            if (isActive) {
                                currentButton.style.backgroundColor = "#ffffff"; // Activo
                                currentButton.style.borderBottom = "2px solid #007bff";
                                currentButton.style.fontWeight = "bold";
                            } else {
                                currentButton.style.backgroundColor = "#f0f0f0"; // Inactivo
                                currentButton.style.borderBottom = "none";
                                currentButton.style.fontWeight = "normal";
                            }
                        }
                        // Llamar a la función de renderizado correspondiente
                        if (isActive) {
                            if (tabLabel_inner === "Espe")
                            {
                                const ul = document.getElementById("excludedWordsList");
                                if (ul && typeof renderExcludedWordsList === 'function') renderExcludedWordsList(ul);
                            }
                            else if (tabLabel_inner === "Dicc")
                            {
                                const ulDict = document.getElementById("dictionaryWordsList");
                                if (ulDict && typeof renderDictionaryList === 'function') renderDictionaryList(ulDict);
                            }
                            else if (tabLabel_inner === "Reemp")
                            {
                            const ulReemplazos = document.getElementById("replacementsListElementID");
                                if (ulReemplazos && typeof renderReplacementsList === 'function') renderReplacementsList(ulReemplazos);
                            }
                        }
                    });
                });
                tabButtons[label] = btn;
                tabsContainer.appendChild(btn);
            });
            tabPane.appendChild(tabsContainer);

            // Crear los divs contenedores para el contenido de cada pestaña
            tabNames.forEach(({ label }) => {
                const contentDiv = document.createElement("div");
                contentDiv.style.display = label === tabNames[0].label ? "block" : "none"; // Mostrar solo la primera
                contentDiv.style.padding = "10px";
                tabContents[label] = contentDiv; // Guardar referencia
                tabPane.appendChild(contentDiv);
            });
            // --- POBLAR EL CONTENIDO DE CADA PESTAÑA ---

            // 4. Poblar el contenido de la pestaña "General"
            const containerGeneral = tabContents["Gene"];
            if (containerGeneral)
            {
                let initialUsernameAttempt =
                "Pendiente"; // Para la etiqueta simplificada
                // No es necesario el polling complejo si solo es para la lógica
                // interna del checkbox
                if (typeof W !== 'undefined' && W.loginManager &&
                    W.loginManager.user && W.loginManager.user.userName)
                {
                    initialUsernameAttempt =
                    W.loginManager.user
                        .userName; // Se usará internamente en processNextPlace
                }
                const mainTitle = document.createElement("h3");
                mainTitle.textContent = "NormliZer";
                mainTitle.style.textAlign = "center";
                mainTitle.style.fontSize = "18px";
                mainTitle.style.marginBottom = "2px";
                containerGeneral.appendChild(mainTitle);

                const versionInfo = document.createElement("div");
                versionInfo.textContent =
                "V. " + VERSION; // VERSION global
                versionInfo.style.textAlign = "right";
                versionInfo.style.fontSize = "10px";
                versionInfo.style.color = "#777";
                versionInfo.style.marginBottom = "15px";
                containerGeneral.appendChild(versionInfo);

                const normSectionTitle = document.createElement("h4");
                normSectionTitle.textContent = "Análisis de Nombres de Places";
                normSectionTitle.style.fontSize = "15px";
                normSectionTitle.style.marginTop = "10px";
                normSectionTitle.style.marginBottom = "5px";
                normSectionTitle.style.borderBottom = "1px solid #eee";
                normSectionTitle.style.paddingBottom = "3px";
                containerGeneral.appendChild(normSectionTitle);

                const scanButton = document.createElement("button");
                scanButton.textContent = "Start Scan...";
                scanButton.setAttribute("type", "button");
                scanButton.style.marginBottom = "10px";
                scanButton.style.width = "100%";
                scanButton.style.padding = "8px";
                scanButton.style.border = "none";
                scanButton.style.borderRadius = "4px";
                scanButton.style.backgroundColor = "#007bff";
                scanButton.style.color = "#fff";
                scanButton.style.cursor = "pointer";
                scanButton.addEventListener("click", () => {
                    const places = getVisiblePlaces();
                    const outputDiv =
                    document.getElementById("wme-normalization-tab-output");
                    if (!outputDiv)
                    { // Mover esta verificación antes
                    // console.error("Div de salida (wme-normalization-tab-output) no encontrado en el tab.");
                        return;
                    }
                    if (places.length === 0)
                    {
                        outputDiv.textContent =  "No se encontraron lugares visibles para analizar.";
                        return;
                    }
                    const maxPlacesInput =  document.getElementById("maxPlacesInput");
                    const maxPlacesToScan = parseInt(maxPlacesInput?.value || "100", 10);
                    const scannedCount = Math.min(places.length, maxPlacesToScan);
                    outputDiv.textContent = `Escaneando ${scannedCount} lugares...`;
                    setTimeout(() => {renderPlacesInFloatingPanel(places.slice(0, maxPlacesToScan));
                    }, 10);
                });
                containerGeneral.appendChild(scanButton);

                const maxWrapper = document.createElement("div");
                maxWrapper.style.display = "flex";
                maxWrapper.style.alignItems = "center";
                maxWrapper.style.gap = "8px";
                maxWrapper.style.marginBottom = "8px";
                const maxLabel = document.createElement("label");
                maxLabel.textContent = "Máximo de places a revisar:";
                maxLabel.style.fontSize = "13px";
                maxWrapper.appendChild(maxLabel);
                const maxInput = document.createElement("input");
                maxInput.type = "number";
                maxInput.id = "maxPlacesInput";
                maxInput.min = "1";
                maxInput.value = "100";
                maxInput.style.width = "80px";
                maxWrapper.appendChild(maxInput);
                containerGeneral.appendChild(maxWrapper);

                const presets = [ 25, 50, 100, 250, 500 ];
                const presetContainer = document.createElement("div");
                presetContainer.style.textAlign = "center";
                presetContainer.style.marginBottom = "8px";
                presets.forEach(preset => {
                    const btn = document.createElement("button");
                    btn.textContent = preset.toString();
                    btn.style.margin = "2px";
                    btn.style.padding = "4px 6px";
                    btn.addEventListener("click", () => {
                        if (maxInput)
                            maxInput.value = preset.toString();
                    });
                    presetContainer.appendChild(btn);
                });
                containerGeneral.appendChild(presetContainer);

                // Checkbox para recomendar categorías
                const recommendCategoriesWrapper = document.createElement("div");
                recommendCategoriesWrapper.style.marginTop = "10px";
                recommendCategoriesWrapper.style.marginBottom = "5px";
                recommendCategoriesWrapper.style.display = "flex";
                recommendCategoriesWrapper.style.alignItems = "center";

                const recommendCategoriesCheckbox = document.createElement("input");
                recommendCategoriesCheckbox.type = "checkbox";
                recommendCategoriesCheckbox.id = "chk-recommend-categories";
                recommendCategoriesCheckbox.style.marginRight = "5px";

                // Recuperar el estado guardado del checkbox
                const savedCategoryRecommendationState = localStorage.getItem("wme_pln_recommend_categories");
                recommendCategoriesCheckbox.checked = (savedCategoryRecommendationState === "true");

                const recommendCategoriesLabel = document.createElement("label");
                recommendCategoriesLabel.textContent = "Recomendar categorías";
                recommendCategoriesLabel.htmlFor = "chk-recommend-categories";
                recommendCategoriesLabel.style.fontSize = "13px";
                recommendCategoriesLabel.style.cursor = "pointer";

                recommendCategoriesWrapper.appendChild(recommendCategoriesCheckbox);
                recommendCategoriesWrapper.appendChild(recommendCategoriesLabel);
                containerGeneral.appendChild(recommendCategoriesWrapper);

                // Guardar el estado del checkbox cada vez que cambia
                recommendCategoriesCheckbox.addEventListener("change", () => {
                    localStorage.setItem("wme_pln_recommend_categories", recommendCategoriesCheckbox.checked ? "true" : "false");
                });                

                // --- NUEVO: Checkbox para omitir mis ediciones ---
                const hideMyEditsWrapper = document.createElement("div");
                hideMyEditsWrapper.style.marginTop = "10px";
                hideMyEditsWrapper.style.marginBottom = "5px";
                hideMyEditsWrapper.style.display = "flex";
                hideMyEditsWrapper.style.alignItems = "center";

                const hideMyEditsCheckbox = document.createElement("input");
                hideMyEditsCheckbox.type = "checkbox";
                hideMyEditsCheckbox.id = "chk-hide-my-edits";
                hideMyEditsCheckbox.style.marginRight = "5px";

                const hideMyEditsLabel = document.createElement("label");

                let labelText = "Omitir lugares editados por mí";
                let currentUserName = null;
                if (typeof W !== 'undefined' && W.loginManager && W.loginManager.user && W.loginManager.user.userName) {
                    currentUserName = W.loginManager.user.userName;
                    labelText += ` (${currentUserName})`;
                } else {
                    labelText += " (Usuario no detectado)";
                }
                hideMyEditsLabel.textContent = labelText;

                hideMyEditsLabel.htmlFor = "chk-hide-my-edits";
                hideMyEditsLabel.style.fontSize = "13px";
                hideMyEditsLabel.style.cursor = "pointer";

                hideMyEditsWrapper.appendChild(hideMyEditsCheckbox);
                hideMyEditsWrapper.appendChild(hideMyEditsLabel);
                // containerGeneral.appendChild(hideMyEditsWrapper);


                const tabProgressWrapper = document.createElement("div");
                tabProgressWrapper.style.margin = "10px 0";
                tabProgressWrapper.style.height = "18px";
                tabProgressWrapper.style.backgroundColor = "transparent";
                const tabProgressBar = document.createElement("div");
                tabProgressBar.style.height = "100%";
                tabProgressBar.style.width = "0%";
                tabProgressBar.style.backgroundColor = "#007bff";
                tabProgressBar.style.transition = "width 0.2s";
                tabProgressBar.id = "progressBarInnerTab";
                tabProgressWrapper.appendChild(tabProgressBar);
                containerGeneral.appendChild(tabProgressWrapper);

                const tabProgressText = document.createElement("div");
                tabProgressText.style.fontSize = "12px";
                tabProgressText.style.marginTop = "5px";
                tabProgressText.id = "progressBarTextTab";
                tabProgressText.textContent = "Progreso: 0% (0/0)";
                containerGeneral.appendChild(tabProgressText);

                const outputNormalizationInTab = document.createElement("div");
                outputNormalizationInTab.id = "wme-normalization-tab-output";
                outputNormalizationInTab.style.fontSize = "12px";
                outputNormalizationInTab.style.minHeight = "20px";
                outputNormalizationInTab.style.padding = "5px";
                outputNormalizationInTab.style.marginBottom = "15px";
                outputNormalizationInTab.textContent =
                "Presiona 'Start Scan...' para analizar los places visibles.";
                containerGeneral.appendChild(outputNormalizationInTab);
            }
            else
            {
                console.error("[WME PLN] No se pudo poblar la pestaña 'General' porque su contenedor no existe.");
            }

            // 5. Poblar las otras pestañas
            if (tabContents["Espe"])
            {
                createExcludedWordsManager(tabContents["Espe"]) ;
            }
            else
            {
                console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Especiales'.");
            }

            if (tabContents["Dicc"])
            {
                createDictionaryManager(tabContents["Dicc"]);
            }
            else
            {
                console.error(
                "[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Diccionario'.");
            }

            // --- LLAMADA A LA FUNCIÓN PARA POBLAR LA NUEVA PESTAÑA "Reemplazos"
            // ---
            if (tabContents["Reemp"])
            {
                createReplacementsManager(tabContents["Reemp"]); // Esta es la llamada clave
            }
            else
            {
                console.error("[WME PLN] No se pudo encontrar el contenedor para la pestaña 'Reemplazos'.");
            }
        }
        catch (error)
        {
            console.error("[WME PLN] Error catastrófico creando la pestaña lateral:", error, error.stack);
        }
    } // Fin de createSidebarTab


    // 2. Esperar a que Waze API esté disponible
    function waitForSidebarAPI()
    {
        // Comprobar si Waze API está disponible
        if (W && W.userscripts && W.userscripts.registerSidebarTab)
        {
            const savedExcluded = localStorage.getItem("excludedWordsList");
            if (savedExcluded)
            {
                try
                {
                    const parsed = JSON.parse(savedExcluded);
                    excludedWords = new Set(); // Reinicializa el Set
                    excludedWordsMap = new Map(); // Reinicializa el Map
                    parsed.forEach(word => { // parsed es el array del JSON
                        excludedWords.add(word);
                        const firstChar = word.charAt(0).toLowerCase();
                        if (!excludedWordsMap.has(firstChar)) {
                            excludedWordsMap.set(firstChar, new Set());
                        }
                        excludedWordsMap.get(firstChar).add(word);
                    });
                    /*   console.log(
                         "[WME PLN] Palabras especiales restauradas desde
                       localStorage:", Array.from(excludedWords));*/
                }
                catch (e)
                {
                    /*console.error(
                      "[WME PLN] Error al cargar excludedWordsList del localStorage:",
                      e);*/
                    excludedWords = new Set();
                }
            }
            else
            {
                excludedWords = new Set();
                /*  console.log(
                    "[WME PLN] No se encontraron palabras especiales en
                   localStorage.");*/
            }
            // --- Cargar diccionario desde localStorage ---
            const savedDictionary = localStorage.getItem("dictionaryWordsList");
            if (savedDictionary)
            {
                try
                {
                    const parsed = JSON.parse(savedDictionary);
                    window.dictionaryWords = new Set(parsed);
                    // Crear el índice de palabras por letra
                    window.dictionaryIndex = {};
                    // Iterar sobre las palabras y agregarlas al índice
                    parsed.forEach(word => {
                        const letter = word.charAt(0).toLowerCase();
                        if (!window.dictionaryIndex[letter])                        
                            window.dictionaryIndex[letter] = [];                        
                        window.dictionaryIndex[letter].push(word);
                    });                   
                }
                catch (e)
                {
                    console.error("[WME PLN] Error al cargar dictionaryWordsList del localStorage:", e);
                    window.dictionaryWords = new Set();
                    window.dictionaryIndex = {};
                }
            }
            else
            {
                window.dictionaryWords = new Set();
                window.dictionaryIndex = {};
                //  console.log("[WME PLN] No se encontró diccionario en
                //  localStorage.");
            }
            // Esto añadirá nuevas palabras del Excel a window.dictionaryWords
            // y se encarga de guardar en localStorage después.
            // Se hace de forma asíncrona pero no bloquea la UI.
            loadDictionaryWordsFromSheet().then(() => {
                console.log('[WME PLN] Carga del diccionario desde Google Sheets finalizada.');
                }).catch(err => {
                    console.error('[WME PLN] Fallo en la carga del diccionario desde Google Sheets:', err);
                });
            // --- Cargar palabras de reemplazo desde localStorage ---
            loadReplacementWordsFromStorage(); 
            // La llamada a waitForWazeAPI ya se encarga de la lógica de dynamicCategoriesLoaded.
            waitForWazeAPI(() => { createSidebarTab(); });
        }
        else
        {
            // console.log("[WME PLN] Esperando W.userscripts API...");
            setTimeout(waitForSidebarAPI, 1000);
        }
    }// Fin de waitForSidebarAPI
    // 1. normalizePlaceName
    function normalizePlaceName(word) {
        //console.log("[NORMALIZER] Analizando nombre:", word);
        if (!word || typeof word !== "string") {
            return "";
        }

        // Manejar palabras con "/" recursivamente
        if (word.includes("/")) {
            if (word === "/") return "/";
            return word.split("/").map(part => normalizePlaceName(part.trim())).join("/");
        }

        // Regla 1: Si la palabra es SOLO números, mantenerla tal cual. (Prioridad alta)
        if (/^[0-9]+$/.test(word)) {
            return word;
        }

        // Regla 2: Números romanos: todo en mayúsculas. No elimina puntos.
        const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
        if (romanRegexStrict.test(word) && word.toUpperCase() !== "MI" && word.toUpperCase() !== "DI" && word.toUpperCase() !== "SI") 
            return word.toUpperCase();
        
        // Regla 3: Si hay un número seguido de letra (sin espacio), capitalizar la letra (Ej: "22a" -> "22A")
        word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`);

        // Regla 4: Acrónimos/Palabras con puntos/letras mayúsculas que deben mantenerse.
        // Esto es para "St." o "U.S.A." o "EPM", "SURA"
        // NOTA: originalNameFull ya no tiene emoticones gracias a `processNextPlace`
        if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && (word.includes('.') || /^[A-ZÁÉÍÓÚÑ]+$/.test(word))) {
            // Asegurarse de que no sea "MI", "DI", "SI" si están en mayúsculas accidentales
            if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI" || word.toUpperCase() === "SI") 
                return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();            
            return word; // Mantener como está
        }
        // Regla 5: Capitalización estándar para el resto de las palabras.
        // Esta será la regla para la mayoría de las palabras que no caen en las anteriores.
        let normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        return normalizedWord;
    }// Fin de normalizePlaceName

    // Función para escapar caracteres especiales en una cadena para usar en regex
    function applyWordsToStartMovement(name)
    {
        let newName = name;
        // Asegurarse de que window.swapWords exista y no esté vacío
        if (!window.swapWords || window.swapWords.length === 0) {
            return newName; // No hay palabras para mover
        }
        // Ordenar las palabras swap por longitud descendente para procesar primero las frases más largas.
        // Esto es crucial para evitar que una palabra corta (ej. "Club") se mueva antes que una frase más larga que la contiene
        // (ej. "Club Campestre"), si ambas estuvieran en la lista.
        const sortedSwapWords = [...window.swapWords].sort((a, b) => b.length - a.length);

        for (const swapWord of sortedSwapWords) {
            // Regex para encontrar la palabra swap al final del nombre.
            // \s* : Cero o más espacios antes de la palabra.
            // (${escapeRegExp(swapWord)}) : Captura la palabra swap (escapando caracteres especiales de regex).
            // \s*$ : Cero o más espacios al final del nombre, seguido del fin de la cadena.
            // 'i' : Para búsqueda insensible a mayúsculas/minúsculas.
            const regex = new RegExp(`\\s*(${escapeRegExp(swapWord)})\\s*$`, 'i');

            if (regex.test(newName)) {
                // Captura la parte del nombre que coincide con la palabra swap al final.
                const match = newName.match(regex);
                const matchedSwapWord = match[1]; // La palabra/frase real que coincidió (ej. "apartamentos", "Urbanización")

                // Elimina la palabra swap del final del nombre para obtener el resto.
                const remainingName = newName.replace(regex, '').trim();

                // Capitaliza la palabra movida para que aparezca correctamente al inicio.
                // Si ya está correctamente capitalizada (ej. "Urbanización"), se mantiene así.
                const capitalizedSwapWord = matchedSwapWord.charAt(0).toUpperCase() + matchedSwapWord.slice(1).toLowerCase();

                // Reconstruye el nombre: Palabra movida + espacio + resto del nombre.
                newName = `${capitalizedSwapWord} ${remainingName}`.trim();

                // Una vez que se mueve una palabra, asumimos que no hay más movimientos
                // que hacer con otras palabras swap para esta misma entrada,
                // a menos que quieras permitir múltiples movimientos, lo cual
                // complicaría la lógica. Por ahora, nos detenemos en la primera coincidencia.
                break;
            }
        }
        return newName;
    }

    function createCategoryDropdown(currentCategoryKey, rowIndex, venue)
    {
        const select = document.createElement("select");
        select.style.padding = "4px";
        select.style.borderRadius = "4px";
        select.style.fontSize = "12px";
        select.title = "Selecciona una categoría";
        select.id = `categoryDropdown-${rowIndex}`;

        Object.entries(categoryIcons).forEach(([key, value]) => {
            const option = document.createElement("option");
            option.value = key;
            option.textContent = `${value.icon} ${value.en}`;
            if (key === currentCategoryKey) {
                option.selected = true;
            }
            select.appendChild(option);
        });
        // Evento: al cambiar la categoría
        select.addEventListener("change", (e) => {
            const selectedCategory = e.target.value;
            if (!venue || !venue.model || !venue.model.attributes) {
                console.error("Venue inválido al intentar actualizar la categoría");
                return;
            }

            // Actualizar la categoría en el modelo
            venue.model.attributes.categories = [selectedCategory];
            venue.model.save();

            // Mensaje opcional de confirmación
            WazeWrap.Alerts.success("Categoría actualizada", `Nueva categoría: ${categoryIcons[selectedCategory].en}`);
        });

        return select;
    }

    function normalizeWordInternal(word, isFirstWordInSequence = false, isInsideQuotesOrParentheses = false)
    {
        if (!word || typeof word !== "string") return "";

        // Casos especiales "MI" y "DI" tienen la MÁS ALTA prioridad.
        if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI" || word.toUpperCase() === "SI")
        {
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        // Usar la regex insensible para la detección de romanos
        const romanRegexInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;

        // Si es un número romano (y no es "MI" o "DI", aunque ya se cubrió arriba), convertir a mayúsculas.
        if (romanRegexInsensitive.test(word)) { // No es necesario verificar MI/DI  de nuevo debido a la primera condición.
            return word.toUpperCase();
        }

        word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`);

        let resultWord;
        if (isInsideQuotesOrParentheses && !isFirstWordInSequence && commonWords.includes(word.toLowerCase()))
        {
            resultWord = word.toLowerCase();
        } else if (/^[0-9]+$/.test(word)) {
            resultWord = word;
        } else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && word.includes('.')) {
            // Mantener "St." dentro de comillas/paréntesis. No debería afectar a "MI".
            resultWord = word;
        }
         else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1) {
            // Mantener acrónimos sin puntos (ej. "ABC"). "MI" ya no caerá .
            resultWord = word;
        }
         else {
            // Capitalización estándar para todo lo demás.
            resultWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        return resultWord;
    }

    // 3. La función postProcessQuotesAndParentheses (CORREGIDA de la respuesta anterior)
    function postProcessQuotesAndParentheses(text)
    {
        if (typeof text !== 'string') return text;

        // Normalizar contenido dentro de comillas dobles
        text = text.replace(/"([^"]*)"/g, (match, content) => {
            const trimmedContent = content.trim();
            if (trimmedContent === "") return '""';

            const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0);
            const normalizedWordsInside = wordsInside.map((singleWord, index) => {
                return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses
            }).join(' ');
            return `"${normalizedWordsInside}"`; // Sin espacios extra
        });

        // Normalizar contenido dentro de paréntesis
        text = text.replace(/\(([^)]*)\)/g, (match, content) => {
            const trimmedContent = content.trim();
            if (trimmedContent === "") return '()';

            const wordsInside = trimmedContent.split(/\s+/).filter(w => w.length > 0);
            const normalizedWordsInside = wordsInside.map((singleWord, index) => {
                return normalizeWordInternal(singleWord, index === 0, true); // true para isInsideQuotesOrParentheses
            }).join(' ');
            return `(${normalizedWordsInside})`; // Sin espacios extra
        });

        return text.replace(/\s+/g, ' ').trim(); // Limpieza final general
    }
    // === Palabras especiales ===
    let excludedWords = new Set(); // Mantenemos el Set para facilitar el renderizado original
    let excludedWordsMap = new Map(); // <-- NUEVO: Para la búsqueda optimizada    
    let dictionaryWords = new Set(); // O window.dictionaryWords = new Set();

    function createExcludedWordsManager(parentContainer)
    {
        const section = document.createElement("div");
        section.id = "excludedWordsManagerSection"; // ID para la sección
        section.style.marginTop = "20px";
        section.style.borderTop = "1px solid #ccc";
        section.style.paddingTop = "10px";

        const title =
            document.createElement("h4"); // Cambiado a h4 para jerarquía
        title.textContent = "Gestión de Palabras Especiales";
        title.style.fontSize =
            "15px"; // Consistente con el otro título de sección
        title.style.marginBottom = "10px"; // Más espacio abajo
        section.appendChild(title);

        const addControlsContainer = document.createElement("div");
        addControlsContainer.style.display = "flex";
        addControlsContainer.style.gap = "8px";
        addControlsContainer.style.marginBottom = "8px";
        addControlsContainer.style.alignItems =
            "center"; // Alinear verticalmente

        const input = document.createElement("input");
        input.type = "text";
        input.placeholder = "Nueva palabra o frase";
        input.style.flexGrow = "1";
        input.style.padding = "6px"; // Mejor padding
        input.style.border = "1px solid #ccc";
        input.style.borderRadius = "3px";
        addControlsContainer.appendChild(input);

        const addBtn = document.createElement("button");
        addBtn.textContent = "Añadir";
        addBtn.style.padding = "6px 10px"; // Mejor padding
        addBtn.style.cursor = "pointer";

        addBtn.addEventListener("click", function() {
            const newWord = input.value.trim();
            const validation = isValidExcludedWord(newWord);
            if (!validation.valid)
            {
                alert(validation.msg);
                return;
            }
            // isValidExcludedWord ya comprueba duplicados en excludedWords y
            // commonWords y ahora también si existe en dictionaryWords.
            excludedWords.add(newWord);
            const firstCharNew = newWord.charAt(0).toLowerCase();
            if (!excludedWordsMap.has(firstCharNew))
            {
                excludedWordsMap.set(firstCharNew, new Set());
            }
            excludedWordsMap.get(firstCharNew).add(newWord); // Añadir al Map optimizado
            input.value = "";
            renderExcludedWordsList(
                document.getElementById("excludedWordsList"));
            saveExcludedWordsToLocalStorage(); 
        });
        addControlsContainer.appendChild(addBtn);
        section.appendChild(addControlsContainer);

        const actionButtonsContainer = document.createElement("div");
        actionButtonsContainer.style.display = "flex";
        actionButtonsContainer.style.gap = "8px";
        actionButtonsContainer.style.marginBottom = "10px"; // Más espacio

        const exportBtn = document.createElement("button");
        exportBtn.textContent = "Exportar"; // Más corto
        exportBtn.title = "Exportar Lista a XML";
        exportBtn.style.padding = "6px 10px";
        exportBtn.style.cursor = "pointer";

        exportBtn.addEventListener("click", exportSharedDataToXml);
        actionButtonsContainer.appendChild(exportBtn);

        const clearBtn = document.createElement("button");
        clearBtn.textContent = "Limpiar"; // Más corto
        clearBtn.title = "Limpiar toda la lista";
        clearBtn.style.padding = "6px 10px";
        clearBtn.style.cursor = "pointer";
        clearBtn.addEventListener("click", function() {
            if (
                confirm(
                "¿Estás seguro de que deseas eliminar TODAS las palabras de la lista?"))
            {
                excludedWords.clear();
                renderExcludedWordsList(document.getElementById("excludedWordsList")); // Pasar el elemento UL
            }
        });
        actionButtonsContainer.appendChild(clearBtn);
        section.appendChild(actionButtonsContainer);

        const search = document.createElement("input");
        search.type = "text";
        search.placeholder = "Buscar en especiales...";
        search.style.display = "block";
        search.style.width = "calc(100% - 14px)"; // Considerar padding y borde
        search.style.padding = "6px";
        search.style.border = "1px solid #ccc";
        search.style.borderRadius = "3px";
        search.style.marginBottom = "5px";

        search.addEventListener("input", () => {
            // Pasar el ulElement directamente
            renderExcludedWordsList(
                document.getElementById("excludedWordsList"),
                search.value.trim());
        });
        section.appendChild(search);

        const listContainerElement = document.createElement("ul");
        listContainerElement.id = "excludedWordsList"; // Este es el UL
        listContainerElement.style.maxHeight = "150px";
        listContainerElement.style.overflowY = "auto";
        listContainerElement.style.border = "1px solid #ddd";
        listContainerElement.style.padding = "5px"; // Padding interno
        listContainerElement.style.margin = "0";    // Resetear margen
        listContainerElement.style.background = "#fff";
        listContainerElement.style.listStyle = "none";
        section.appendChild(listContainerElement);

        const dropArea = document.createElement("div");
        dropArea.textContent =  "Arrastra aquí el archivo XML de palabras especiales";
        dropArea.style.border = "2px dashed #ccc"; // Borde más visible
        dropArea.style.borderRadius = "4px";
        dropArea.style.padding = "15px"; // Más padding
        dropArea.style.marginTop = "10px";
        dropArea.style.textAlign = "center";
        dropArea.style.background = "#f9f9f9";
        dropArea.style.color = "#555";
        dropArea.addEventListener("dragover", (e) => {
            e.preventDefault();
            dropArea.style.background = "#e9e9e9";
            dropArea.style.borderColor = "#aaa";
        });
        dropArea.addEventListener("dragleave", () => {
            dropArea.style.background = "#f9f9f9";
            dropArea.style.borderColor = "#ccc";
        });
        dropArea.addEventListener("drop", (e) => {
            e.preventDefault();
            dropArea.style.background = "#f9f9f9";
            handleXmlFileDrop(e.dataTransfer.files[0]);

            if (file &&
                (file.type === "text/xml" || file.name.endsWith(".xml")))
            {
                const reader = new FileReader();
                reader.onload = function(evt) {
                    try
                    {
                        const parser = new DOMParser();
                        const xmlDoc = parser.parseFromString(
                            evt.target.result, "application/xml");
                        const parserError = xmlDoc.querySelector("parsererror");
                        if (parserError)
                        {
                            alert("Error al parsear el archivo XML.");
                            return;
                        }
                        // Detectar raíz
                        const rootTag =
                            xmlDoc.documentElement.tagName.toLowerCase();
                        if (rootTag !== "excludedwords" &&
                            rootTag !== "diccionario")
                        {
                            alert(
                                "El archivo XML no es válido. Debe tener <ExcludedWords> o <diccionario> como raíz.");
                            return;
                        }
                        // Importar palabras
                        const words = xmlDoc.getElementsByTagName("word");
                        let newWordsAddedCount = 0;
                        for (let i = 0; i < words.length; i++)
                        {
                            const val = words[i].textContent.trim();
                            if (val && !excludedWords.has(val))
                            {
                                excludedWords.add(val);
                                newWordsAddedCount++;
                            }
                        }
                        // Importar reemplazos si existen
                        const replacements =
                            xmlDoc.getElementsByTagName("replacement");
                        for (let i = 0; i < replacements.length; i++)
                        {
                            const from = replacements[i].getAttribute("from");
                            const to = replacements[i].textContent.trim();
                            if (from && to)
                            {
                                replacementWords[from] = to;
                            }
                        }
                        renderExcludedWordsList(
                            document.getElementById("excludedWordsList"));
                        alert(`Importación completada. Palabras nuevas: ${
                            newWordsAddedCount}`);
                    }
                    catch (err)
                    {
                        alert("Error procesando el archivo XML.");
                    }
                };
                reader.readAsText(file);
            }
            else
            {
                alert("Por favor, arrastra un archivo XML válido.");
            }
        });
        section.appendChild(dropArea);
        parentContainer.appendChild(section);
    }

    // === Diccionario ===
    function createDictionaryManager(parentContainer)
    {
        const section = document.createElement("div");
        section.id = "dictionaryManagerSection";
        section.style.marginTop = "20px";
        section.style.borderTop = "1px solid #ccc";
        section.style.paddingTop = "10px";

        const title = document.createElement("h4");
        title.textContent = "Gestión del Diccionario";
        title.style.fontSize = "15px";
        title.style.marginBottom = "10px";
        section.appendChild(title);

        const addControlsContainer = document.createElement("div");
        addControlsContainer.style.display = "flex";
        addControlsContainer.style.gap = "8px";
        addControlsContainer.style.marginBottom = "8px";
        addControlsContainer.style.alignItems = "center"; // Alinear verticalmente

        const input = document.createElement("input");
        input.type = "text";
        input.placeholder = "Nueva palabra";
        input.style.flexGrow = "1";
        input.style.padding = "6px"; // Mejor padding
        input.style.border = "1px solid #ccc";
        input.style.borderRadius = "3px";
        addControlsContainer.appendChild(input);

        const addBtn = document.createElement("button");
        addBtn.textContent = "Añadir";
        addBtn.style.padding = "6px 10px"; // Mejor padding
        addBtn.style.cursor = "pointer";
        addBtn.addEventListener("click", function() {
            const newWord = input.value.trim();
            if (newWord)
            {
                const lowerNewWord = newWord.toLowerCase();

                const alreadyExists =
                  Array.from(window.dictionaryWords)
                    .some(w => w.toLowerCase() === lowerNewWord);

                if (commonWords.includes(lowerNewWord))
                {
                    alert(
                      "La palabra es muy común y no debe agregarse a la lista.");
                    return;
                }

                if (alreadyExists)
                {
                    alert("La palabra ya está en la lista.");
                    return;
                }

                window.dictionaryWords.add(lowerNewWord);
                input.value = "";
                renderDictionaryList(
                  document.getElementById("dictionaryWordsList"));

            }
        });

        addControlsContainer.appendChild(addBtn);
        section.appendChild(addControlsContainer);

        const actionButtonsContainer = document.createElement("div");
        actionButtonsContainer.style.display = "flex";
        actionButtonsContainer.style.gap = "8px";
        actionButtonsContainer.style.marginBottom = "10px"; // Más espacio

        const exportBtn = document.createElement("button");
        exportBtn.textContent = "Exportar"; // Más corto
        exportBtn.title = "Exportar Diccionario a XML";
        exportBtn.style.padding = "6px 10px";
        exportBtn.style.cursor = "pointer";
        exportBtn.addEventListener("click", exportDictionaryWordsList);
        actionButtonsContainer.appendChild(exportBtn);

        const clearBtn = document.createElement("button");
        clearBtn.textContent = "Limpiar"; // Más corto
        clearBtn.title = "Limpiar toda la lista";
        clearBtn.style.padding = "6px 10px";
        clearBtn.style.cursor = "pointer";
        clearBtn.addEventListener("click", function() {
            if (
              confirm(
                "¿Estás seguro de que deseas eliminar TODAS las palabras del diccionario?"))
            {
                window.dictionaryWords.clear();
                renderDictionaryList(document.getElementById(
                  "dictionaryWordsList")); // Pasar el elemento UL
            }
        });
        actionButtonsContainer.appendChild(clearBtn);
        section.appendChild(actionButtonsContainer);

        // Diccionario: búsqueda
        const search = document.createElement("input");
        search.type = "text";
        search.placeholder = "Buscar en diccionario...";
        search.style.display = "block";
        search.style.width = "calc(100% - 14px)";
        search.style.padding = "6px";
        search.style.border = "1px solid #ccc";
        search.style.borderRadius = "3px";
        search.style.marginTop = "5px";
        // On search input, render filtered list
        search.addEventListener("input", () => {
            renderDictionaryList(document.getElementById("dictionaryWordsList"),
                                 search.value.trim());
        });
        section.appendChild(search);

        // Lista UL para mostrar palabras del diccionario
        const listContainerElement = document.createElement("ul");
        listContainerElement.id = "dictionaryWordsList";
        listContainerElement.style.maxHeight = "150px";
        listContainerElement.style.overflowY = "auto";
        listContainerElement.style.border = "1px solid #ddd";
        listContainerElement.style.padding = "5px";
        listContainerElement.style.margin = "0";
        listContainerElement.style.background = "#fff";
        listContainerElement.style.listStyle = "none";
        section.appendChild(listContainerElement);

        const dropArea = document.createElement("div");
        dropArea.textContent = "Arrastra aquí el archivo XML del diccionario";
        dropArea.style.border = "2px dashed #ccc";
        dropArea.style.borderRadius = "4px";
        dropArea.style.padding = "15px";
        dropArea.style.marginTop = "10px";
        dropArea.style.textAlign = "center";
        dropArea.style.background = "#f9f9f9";
        dropArea.style.color = "#555";
        dropArea.addEventListener("dragover", (e) => {
            e.preventDefault();
            dropArea.style.background = "#e9e9e9";
            dropArea.style.borderColor = "#aaa";
        });
        dropArea.addEventListener("dragleave", () => {
            dropArea.style.background = "#f9f9f9";
            dropArea.style.borderColor = "#ccc";
        });

        dropArea.addEventListener("drop", (e) => {
            e.preventDefault();
            dropArea.style.background = "#f9f9f9";
            dropArea.style.borderColor = "#ccc";
            const file = e.dataTransfer.files[0];
            if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
            {
                const reader = new FileReader();
                reader.onload = function(evt) {
                    try
                    {
                        const parser = new DOMParser();
                        const xmlDoc = parser.parseFromString(evt.target.result,
                                                              "application/xml");
                        const parserError = xmlDoc.querySelector("parsererror");
                        if (parserError)
                        {
                            console.error("[WME PLN] Error parseando XML:",
                                          parserError.textContent);
                            alert(
                              "Error al parsear el archivo XML del diccionario.");
                            return;
                        }
                        const xmlWords = xmlDoc.querySelectorAll("word");
                        let newWordsAddedCount = 0;
                        for (let i = 0; i < xmlWords.length; i++)
                        {
                            const val = xmlWords[i].textContent.trim();
                            if (val && !window.dictionaryWords.has(val))
                            {
                                window.dictionaryWords.add(val);
                                newWordsAddedCount++;
                            }
                        }
                        if (newWordsAddedCount > 0)
                            console.log(`[WME PLN] ${
                              newWordsAddedCount} nuevas palabras añadidas desde XML.`);

                        // Renderizar la lista en el panel
                        renderDictionaryList(listContainerElement);
                    }
                    catch (err)
                    {
                        alert("Error procesando el diccionario XML.");
                    }
                };
                reader.readAsText(file);
            }
            else
            {
                alert("Por favor, arrastra un archivo XML válido.");
            }
        });
        section.appendChild(dropArea);

        parentContainer.appendChild(section);
        renderDictionaryList(listContainerElement);
    }
    // Carga las palabras excluidas desde localStorage
    function loadReplacementWordsFromStorage()
    {
        const savedReplacements = localStorage.getItem("replacementWordsList");
        if (savedReplacements)
        {
            try
            {
                replacementWords = JSON.parse(savedReplacements);
                if (typeof replacementWords !== 'object' ||
                    replacementWords === null)
                { // Asegurar que sea un objeto
                    replacementWords = {};
                }
            }
            catch (e)
            {
                console.error("[WME PLN] Error cargando lista de reemplazos desde localStorage:", e);
                replacementWords = {};
            }
        }
        else
        {
            replacementWords = {}; // Inicializar si no hay nada guardado
        }
        console.log("[WME PLN] Reemplazos cargados:",
                    Object.keys(replacementWords).length,
                    "reglas.");
    }
    
    // Carga las palabras excluidas desde localStorage
    function saveSwapWordsToStorage()
    {
        localStorage.setItem("swapWords", JSON.stringify(window.swapWords || []));
    }
    // Carga las palabras reemplazo
    function saveReplacementWordsToStorage()
    {
        try
        {
            localStorage.setItem("replacementWordsList",
                                 JSON.stringify(replacementWords));
            // console.log("[WME PLN] Lista de reemplazos guardada en localStorage.");
        }
        catch (e)
        {
            console.error("[WME PLN] Error guardando lista de reemplazos en localStorage:", e);
        }
    }
    // Carga las palabras excluidas desde localStorage
    function saveExcludedWordsToLocalStorage()
    {
        try {
            localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords)));
            // console.log("[WME PLN] Lista de palabras especiales guardada en localStorage.");
        } catch (e) {
            console.error("[WME PLN] Error guardando palabras especiales en localStorage:", e);
        }
    }//

    // Renderiza la lista de reemplazos
    function renderReplacementsList(ulElement)
    {
        //console.log("[WME PLN DEBUG] renderReplacementsList llamada para:", ulElement ? ulElement.id : "Elemento UL nulo");
        if (!ulElement)
        {
            //console.error("[WME PLN] Elemento UL para reemplazos no proporcionado a renderReplacementsList.");
            return;
        }
        ulElement.innerHTML = ""; // Limpiar lista actual
        const entries = Object.entries(replacementWords);

        if (entries.length === 0)
        {
            const li = document.createElement("li");
            li.textContent = "No hay reemplazos definidos.";
            li.style.textAlign = "center";
            li.style.color = "#777";
            li.style.padding = "5px";
            ulElement.appendChild(li);
            return;
        }
        // Ordenar alfabéticamente por la palabra original (from)
        entries.sort((a, b) =>  a[0].toLowerCase().localeCompare(b[0].toLowerCase()));        
        entries.forEach(([ from, to ]) => {
            const li = document.createElement("li");
            li.style.display = "flex";
            li.style.justifyContent = "space-between";
            li.style.alignItems = "center";
            li.style.padding = "4px 2px";
            li.style.borderBottom = "1px solid #f0f0f0";

            const textContainer = document.createElement("div");
            textContainer.style.flexGrow = "1";
            textContainer.style.overflow = "hidden";
            textContainer.style.textOverflow = "ellipsis";
            textContainer.style.whiteSpace = "nowrap";
            textContainer.title = `Reemplazar "${from}" con "${to}"`;

            const fromSpan = document.createElement("span");
            fromSpan.textContent = from;
            fromSpan.style.fontWeight = "bold";
            textContainer.appendChild(fromSpan);

            const arrowSpan = document.createElement("span");
            arrowSpan.textContent = " → ";
            arrowSpan.style.margin = "0 5px";
            textContainer.appendChild(arrowSpan);

            const toSpan = document.createElement("span");
            toSpan.textContent = to;
            toSpan.style.color = "#007bff";
            textContainer.appendChild(toSpan);

            li.appendChild(textContainer);

            // Botón Editar
            const editBtn = document.createElement("button");
            editBtn.innerHTML = "✏️";
            editBtn.title = "Editar este reemplazo";
            editBtn.style.border = "none";
            editBtn.style.background = "transparent";
            editBtn.style.cursor = "pointer";
            editBtn.style.padding = "2px 4px";
            editBtn.style.fontSize = "14px";
            editBtn.style.marginLeft = "4px";
            editBtn.addEventListener("click", () => {
                const newFrom = prompt("Editar texto original:", from);
                if (newFrom === null) return;
                const newTo = prompt("Editar texto de reemplazo:", to);
                if (newTo === null) return;
                if (!newFrom.trim()) {
                    alert("El campo 'Texto Original' es requerido.");
                    return;
                }
                if (newFrom === newTo) {
                    alert("El texto original y el de reemplazo no pueden ser iguales.");
                    return;
                }
                // Si cambia la clave, elimina la anterior
                if (newFrom !== from) delete replacementWords[from];
                replacementWords[newFrom] = newTo;
                renderReplacementsList(ulElement);
                saveReplacementWordsToStorage();
            });

            // Botón Eliminar
            const deleteBtn = document.createElement("button");
            deleteBtn.innerHTML = "🗑️";
            deleteBtn.title = `Eliminar este reemplazo`;
            deleteBtn.style.border = "none";
            deleteBtn.style.background = "transparent";
            deleteBtn.style.cursor = "pointer";
            deleteBtn.style.padding = "2px 4px";
            deleteBtn.style.fontSize = "14px";
            deleteBtn.style.marginLeft = "4px";
            deleteBtn.addEventListener("click", () => {
                if (confirm(`¿Estás seguro de eliminar el reemplazo:\n"${from}" → "${to}"?`))
                {
                    delete replacementWords[from];
                    renderReplacementsList(ulElement);
                    saveReplacementWordsToStorage();
                }
            });

            const btnContainer = document.createElement("span");
            btnContainer.style.display = "flex";
            btnContainer.style.gap = "4px";
            btnContainer.appendChild(editBtn);
            btnContainer.appendChild(deleteBtn);

            li.appendChild(btnContainer);
            ulElement.appendChild(li);
        });
    }
    
    // Exporta las palabras especiales y reemplazos a un archivo XML
    function exportSharedDataToXml()
    {
        if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0 &&
            (!window.swapWords || window.swapWords.length === 0)
        ) {
            alert("No hay palabras especiales, reemplazos ni palabras swap definidos para exportar.");
            return;
        }
        let xmlParts = [];
        // Exportar palabras excluidas
        if (excludedWords.size > 0) {
            xmlParts.push("    <words>");
            Array.from(excludedWords)
                .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
                .forEach(w => xmlParts.push(`        <word>${xmlEscape(w)}</word>`));
            xmlParts.push("    </words>");
        }
        // Exportar reemplazos
        if (Object.keys(replacementWords).length > 0) 
        {
            xmlParts.push("    <replacements>");
            Object.entries(replacementWords)
                .sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase()))
                .forEach(([from, to]) => {
                    xmlParts.push(`        <replacement from="${xmlEscape(from)}">${xmlEscape(to)}</replacement>`);
                });
            xmlParts.push("    </replacements>");
        }
        // Exportar palabras swap en orden de ingreso (sin sort)
        if (window.swapWords && window.swapWords.length > 0) {
            xmlParts.push("    <swapWords>");
            window.swapWords.forEach(val => {
                xmlParts.push(`        <swap value="${xmlEscape(val)}"/>`);
            });
            xmlParts.push("    </swapWords>");
        }

        const xmlContent =
            `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n${xmlParts.join("\n")}\n</ExcludedWords>`;

        const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "wme_normalizer_data_export.xml";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }//exportSharedDataToXml

    // Escapa caracteres especiales para XML
    function handleXmlFileDrop(file)
    {
        if (file && (file.type === "text/xml" || file.name.endsWith(".xml")))
        {
            const reader = new FileReader();
            reader.onload = function(evt) {
                try
                {
                    const parser = new DOMParser();
                    const xmlDoc =
                        parser.parseFromString(evt.target.result, "application/xml");
                    const parserError = xmlDoc.querySelector("parsererror");
                    if (parserError)
                    {
                        alert("Error al parsear el archivo XML: " +
                            parserError.textContent);
                        return;
                    }
                    const rootTag = xmlDoc.documentElement.tagName.toLowerCase();
                    if (rootTag !== "excludedwords")
                    { // Asumiendo que la raíz sigue siendo esta
                        alert(
                            "El archivo XML no es válido. Debe tener <ExcludedWords> como raíz.");
                        return;
                    }

                    let newExcludedAdded = 0;
                    let newReplacementsAdded = 0;
                    let replacementsOverwritten = 0;

                    // Importar palabras excluidas
                    const words = xmlDoc.getElementsByTagName("word");
                    for (let i = 0; i < words.length; i++)
                    {
                        const val = words[i].textContent.trim();
                        if (val && !excludedWords.has(val))
                        {
                            const validation = isValidExcludedWord(val);
                            if (validation.valid)
                            {
                                excludedWords.add(val);
                                newExcludedAdded++;
                            }
                            else
                            {
                                console.warn(`Palabra excluida omitida desde XML "${val}": ${validation.msg}`);
                            }
                        }
                    }

                    // Importar reemplazos
                    const replacements = xmlDoc.getElementsByTagName("replacement");
                    for (let i = 0; i < replacements.length; i++)
                    {
                        const from = replacements[i].getAttribute("from")?.trim();
                        const to = replacements[i].textContent.trim();
                        if (from && to)
                        {
                            if (replacementWords.hasOwnProperty(from) &&
                                replacementWords[from] !== to)
                            {
                                replacementsOverwritten++;
                            }
                            else if (!replacementWords.hasOwnProperty(from))
                            {
                                newReplacementsAdded++;
                            }
                            replacementWords[from] = to;
                        }
                    }

                    // === Importar swapWords, respetando orden ===
                    const swapWordsNode = xmlDoc.querySelector("swapWords");
                    if (swapWordsNode) {
                        if (!window.swapWords) window.swapWords = [];
                        window.swapWords = [];
                        swapWordsNode.querySelectorAll("swap").forEach(swapNode => {
                            const value = swapNode.getAttribute("value");
                            if (value && !window.swapWords.includes(value)) {
                                window.swapWords.push(value);
                                saveSwapWordsToStorage();
                            }
                        });
                    }

                    // Guardar y Re-renderizar AMBAS listas
                    saveExcludedWordsToLocalStorage();
                    saveReplacementWordsToStorage();

                    // Re-renderizar las listas en sus respectivas pestañas si están
                    // visibles o al activarse
                    const excludedListElement =
                        document.getElementById("excludedWordsList");
                    if (excludedListElement)
                        renderExcludedWordsList(excludedListElement);

                    const replacementsListElement =
                        document.getElementById("replacementsListElementID");
                    if (replacementsListElement)
                        renderReplacementsList(replacementsListElement);

                    alert(`Importación completada.\nPalabras Especiales nuevas: ${newExcludedAdded}\nReemplazos nuevos: ${newReplacementsAdded}\nReemplazos sobrescritos: ${replacementsOverwritten}`);
                }
                catch (err)
                {
                    console.error(
                        "[WME PLN] Error procesando el archivo XML importado:", err);
                    alert("Ocurrió un error procesando el archivo XML.");
                }
            };
            reader.readAsText(file);
        }
        else
        {
            alert("Por favor, arrastra un archivo XML válido.");
        }
    }//handleXmlFileDrop

    // Carga las palabras swap desde localStorage
    function loadSwapWordsFromStorage()
    {
        const stored = localStorage.getItem("swapWords");
        if (stored)
        {
            try
            {
                window.swapWords = JSON.parse(stored);
            }
            catch (e)
            {
                window.swapWords = [];
            }
        }
        else
        {
            window.swapWords = [];
        }
    }// loadSwapWordsFromStorage

    // Crea el gestor de reemplazos
    function createReplacementsManager(parentContainer)
    {
        loadSwapWordsFromStorage();
        parentContainer.innerHTML = ''; // Limpiar por si acaso
        // --- Contenedor principal ---
        const title = document.createElement("h4");
        title.textContent = "Gestión de Reemplazos";
        title.style.fontSize = "15px";
        title.style.marginBottom = "10px";
        parentContainer.appendChild(title);
        // --- Dropdown de modo de reemplazo ---
        const modeSelector = document.createElement("select");
        modeSelector.id = "replacementModeSelector";
        modeSelector.style.marginBottom = "10px";
        modeSelector.style.marginTop = "5px";
        // Añadir opciones al selector
        const optionWords = document.createElement("option");
        optionWords.value = "words";
        optionWords.textContent = "Reemplazos de palabras";
        modeSelector.appendChild(optionWords);
        // Añadir opción para swap
        const optionSwap = document.createElement("option");
        optionSwap.value = "swapStart";
        optionSwap.textContent = "Palabras al inicio (swap)";
        modeSelector.appendChild(optionSwap);            
        parentContainer.appendChild(modeSelector);
        //Contenedor para reemplazos y controles
        const replacementsContainer = document.createElement("div");
        replacementsContainer.id = "replacementsContainer";
        // Sección para añadir nuevos reemplazos
        const addSection = document.createElement("div");
        addSection.style.display = "flex";
        addSection.style.gap = "8px";
        addSection.style.marginBottom = "12px";
        addSection.style.alignItems = "flex-end"; // Alinear inputs y botón
        // Contenedores para inputs de texto
        const fromInputContainer = document.createElement("div");
        fromInputContainer.style.flexGrow = "1";
        const fromLabel = document.createElement("label");
        fromLabel.textContent = "Texto Original:";
        fromLabel.style.display = "block";
        fromLabel.style.fontSize = "12px";
        fromLabel.style.marginBottom = "2px";
        // Input para el texto original
        const fromInput = document.createElement("input");
        fromInput.type = "text";
        fromInput.placeholder = "Ej: Urb.";
        fromInput.style.width = "95%"; // Para que quepa bien
        fromInput.style.padding = "6px";
        fromInput.style.border = "1px solid #ccc";
        // Añadir label e input al contenedor
        fromInputContainer.appendChild(fromLabel);
        fromInputContainer.appendChild(fromInput);
        addSection.appendChild(fromInputContainer);
        // Contenedor para el texto de reemplazo
        const toInputContainer = document.createElement("div");
        toInputContainer.style.flexGrow = "1";
        const toLabel = document.createElement("label");
        toLabel.textContent = "Texto de Reemplazo:";
        toLabel.style.display = "block";
        toLabel.style.fontSize = "12px";
        toLabel.style.marginBottom = "2px";
        // Input para el texto de reemplazo
        const toInput = document.createElement("input");
        toInput.type = "text";
        toInput.placeholder = "Ej: Urbanización";
        toInput.style.width = "95%";
        toInput.style.padding = "6px";
        toInput.style.border = "1px solid #ccc";
        toInputContainer.appendChild(toLabel);
        toInputContainer.appendChild(toInput);
        addSection.appendChild(toInputContainer);
        // Atributos para evitar corrección ortográfica
        fromInput.setAttribute('spellcheck', 'false');
        toInput.setAttribute('spellcheck', 'false');
        // Botón para añadir el reemplazo
        const addReplacementBtn = document.createElement("button");
        addReplacementBtn.textContent = "Añadir";
        addReplacementBtn.style.padding = "6px 10px";
        addReplacementBtn.style.cursor = "pointer";
        addReplacementBtn.style.height = "30px"; // Para alinear con los inputs
        addSection.appendChild(addReplacementBtn);
        // Elemento UL para la lista de reemplazos
        const listElement = document.createElement("ul");
        listElement.id = "replacementsListElementID"; // ID ÚNICO para esta lista
        listElement.style.maxHeight = "150px";
        listElement.style.overflowY = "auto";
        listElement.style.border = "1px solid #ddd";
        listElement.style.padding = "8px";
        listElement.style.margin = "0 0 10px 0";
        listElement.style.background = "#fff";
        listElement.style.listStyle = "none";
        // Event listener para el botón "Añadir"
        addReplacementBtn.addEventListener("click", () => {
            const fromValue = fromInput.value.trim();
            const toValue = toInput.value.trim();                
            if (!fromValue)
            {
                alert("El campo 'Texto Original' es requerido.");
                return;
            }
            // Validar que no sea solo caracteres especiales
            if (fromValue === toValue)
            {
                alert("El texto original y el de reemplazo no pueden ser iguales.");
                return;
            }
            // Validar que no sea solo caracteres especiales
            if (replacementWords.hasOwnProperty(fromValue) && replacementWords[fromValue] !== toValue)
            {
                if (!confirm(`El reemplazo para "${fromValue}" ya existe ('${replacementWords[fromValue]}'). ¿Deseas sobrescribirlo con '${toValue}'?`))
                    return;
            }
            replacementWords[fromValue] = toValue;
            fromInput.value = "";
            toInput.value = "";
            // Renderiza toda la lista (más seguro y rápido en la práctica)
            renderReplacementsList(listElement);
            saveReplacementWordsToStorage();
        });
        // Botones de Acción y Drop Area (usarán la lógica compartida)
        const actionButtonsContainer = document.createElement("div");
        actionButtonsContainer.style.display = "flex";
        actionButtonsContainer.style.gap = "8px";
        actionButtonsContainer.style.marginBottom = "10px";
        // Botones de acción
        const exportButton = document.createElement("button");
        exportButton.textContent = "Exportar Todo";
        exportButton.title = "Exportar Excluidas y Reemplazos a XML";
        exportButton.style.padding = "6px 10px";
        exportButton.addEventListener("click", exportSharedDataToXml); // Llamar a la función compartida
        actionButtonsContainer.appendChild(exportButton);
        // Botón para exportar solo reemplazos
        const clearButton = document.createElement("button");
        clearButton.textContent = "Limpiar Reemplazos";
        clearButton.title = "Limpiar solo la lista de reemplazos";
        clearButton.style.padding = "6px 10px";
        clearButton.addEventListener("click", () => {
            if (
                confirm(
                "¿Estás seguro de que deseas eliminar TODOS los reemplazos definidos?"))
            {
                replacementWords = {};
                saveReplacementWordsToStorage();
                renderReplacementsList(listElement);
            }
        });
        actionButtonsContainer.appendChild(clearButton);
        // Botón para importar desde XML
        const dropArea = document.createElement("div");
        dropArea.textContent = "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)";
        dropArea.style.border = "2px dashed #ccc";
        dropArea.style.borderRadius = "4px";
        dropArea.style.padding = "15px";
        dropArea.style.marginTop = "10px";
        dropArea.style.textAlign = "center";
        dropArea.style.background = "#f9f9f9";
        dropArea.style.color = "#555";
        dropArea.addEventListener("dragover", (e) => {
            e.preventDefault();
            dropArea.style.background = "#e9e9e9";
        });
        dropArea.addEventListener("dragleave", () => { dropArea.style.background = "#f9f9f9"; });
        dropArea.addEventListener("drop", (e) => {
            e.preventDefault();
            dropArea.style.background = "#f9f9f9";
            handleXmlFileDrop(e.dataTransfer.files[0]);
        });
        // --- Ensamblar en replacementsContainer ---
        replacementsContainer.appendChild(addSection);
        replacementsContainer.appendChild(listElement);
        replacementsContainer.appendChild(actionButtonsContainer);
        replacementsContainer.appendChild(dropArea);
        parentContainer.appendChild(replacementsContainer);
        // --- Contenedor para swapStart/frases al inicio ---
        const swapContainer = document.createElement("div");
        swapContainer.id = "swapContainer";
        swapContainer.style.display = "none";
        // Título y explicación del swap
        const swapTitle = document.createElement("h4");
        swapTitle.textContent = "Palabras al inicio";
        // Estilo del título
        const swapExplanationBox = document.createElement("div");
        swapExplanationBox.style.background = "#f4f8ff";
        swapExplanationBox.style.borderLeft = "4px solid #2d6df6";
        swapExplanationBox.style.padding = "10px";
        swapExplanationBox.style.margin = "10px 0";
        swapExplanationBox.style.fontSize = "13px";
        swapExplanationBox.style.lineHeight = "1.4";
        swapExplanationBox.innerHTML =
        "<strong>🔄 ¿Qué hace esta lista?</strong><br>" +
        "Las palabras ingresadas aquí se moverán del final al inicio del nombre del lugar si se encuentran al final.<br>" +
        "<em>Ej:</em> “Las Palmas <strong>Urbanización</strong>” → “<strong>Urbanización</strong> Las Palmas”<br>" +
        "<em>Ej:</em> “Tornillos <strong>Ferretería</strong>” → “<strong>Ferretería</strong> Tornillos”";
        // Añadir caja de explicación al contenedor
        swapContainer.appendChild(swapExplanationBox);
        const swapExplanation = document.createElement("p");
        swapExplanation.textContent = "El orden importa: las palabras se evalúan una a una desde el inicio. Si se ordenan alfabéticamente, una más corta podría bloquear otra más específica.";
        swapExplanation.style.fontSize = "12px";
        swapExplanation.style.fontStyle = "italic";
        swapExplanation.style.marginTop = "6px";
        swapExplanation.style.marginBottom = "10px";
        swapExplanation.style.color = "#555";
        // Inserta este nodo justo después del swapTitle, por ejemplo:
        swapContainer.appendChild(swapExplanation);
        swapTitle.style.fontSize = "14px";
        swapTitle.style.marginBottom = "8px";
        swapContainer.appendChild(swapTitle);
        // Contenedor para añadir nuevas palabras swap
        const swapInput = document.createElement("input");
        swapInput.type = "text";
        swapInput.placeholder = "Ej: Urbanización";
        swapInput.style.width = "70%";
        swapInput.style.padding = "6px";
        swapInput.style.marginRight = "8px";
        // Atributos para evitar corrección ortográfica
        const swapBtn = document.createElement("button");
        swapBtn.textContent = "Añadir";
        swapBtn.style.padding = "6px 10px";
        swapBtn.addEventListener("click", () => {
            const val = swapInput.value.trim();
            if (!val || /^[^a-zA-Z0-9]+$/.test(val))
            {
                alert("No se permiten caracteres especiales solos");
                return;
            }
            if (window.swapWords.includes(val))
            {
                alert("Ya existe en la lista.");
                return;
            }
            window.swapWords.push(val); // mantiene orden
            localStorage.setItem("wme_swapWords", JSON.stringify(window.swapWords));
            saveSwapWordsToStorage();  // Guardar en localStorage
            swapInput.value = "";
            renderSwapList();
        });
        swapContainer.appendChild(swapInput);
        swapContainer.appendChild(swapBtn);
        // Añadir campo de búsqueda justo después de swapBtn
        searchSwapInput = document.createElement("input");
        searchSwapInput.type = "text";
        searchSwapInput.placeholder = "Buscar palabra...";
        searchSwapInput.id = "searchSwapInput";
        searchSwapInput.style.width = "70%";
        searchSwapInput.style.padding = "6px";
        searchSwapInput.style.marginTop = "8px";
        searchSwapInput.style.marginBottom = "8px";
        searchSwapInput.style.border = "1px solid #ccc";
        // Escuchar el input para actualizar lista
        searchSwapInput.addEventListener("input", () => {
            renderSwapList(searchSwapInput);
        });
        swapContainer.appendChild(searchSwapInput);
        // Renderiza la lista
        renderSwapList(searchSwapInput);
        parentContainer.appendChild(swapContainer);
        // --- Alternar visibilidad según modo seleccionado ---
        modeSelector.addEventListener("change", () => {
            replacementsContainer.style.display = modeSelector.value === "words" ? "block" : "none";
            swapContainer.style.display = modeSelector.value === "swapStart" ? "block" : "none";
        });
        // --- Función para renderizar la lista de swapWords ---
        function renderSwapList(searchInput = null)
        {
            // Buscar automáticamente el campo si no se pasó como parámetro
            if (!searchInput)
                searchInput = document.getElementById("searchSwapInput");                
            // Asegurarse de que swapContainer existe
            const swapList = swapContainer.querySelector("ul") || (() => {
                const ul = document.createElement("ul");
                ul.id = "swapList";
                ul.style.maxHeight = "120px";
                ul.style.overflowY = "auto";
                ul.style.border = "1px solid #ddd";
                ul.style.padding = "8px";
                ul.style.margin = "10px 0 0 0";
                ul.style.background = "#fff";
                ul.style.listStyle = "none";
                swapContainer.appendChild(ul);
                return ul;
            })();
            swapList.innerHTML = "";
            // Verificar si hay palabras swap definidas
            if (!window.swapWords || window.swapWords.length === 0)
            {
                const li = document.createElement("li");
                li.textContent = "No hay palabras al inicio definidas.";
                li.style.textAlign = "center";
                li.style.color = "#777";
                li.style.padding = "5px";
                swapList.appendChild(li);
                return;
            }
            // Filtrar palabras swap según el término de búsqueda
            const searchTerm = searchSwapInput && searchSwapInput.value ? searchSwapInput.value.trim().toLowerCase() : "";
            let filteredSwapWords = Array.from(window.swapWords);
            // Si hay un término de búsqueda, filtrar la lista
            if (searchTerm)                
                filteredSwapWords = filteredSwapWords.filter(word => word.toLowerCase().includes(searchTerm));
            // Ordenar alfabéticamente
            filteredSwapWords.forEach(word => {
                const li = document.createElement("li");
                li.style.display = "flex";
                li.style.justifyContent = "space-between";
                li.style.alignItems = "center";
                li.style.padding = "4px 2px";
                li.style.borderBottom = "1px solid #f0f0f0";
                // Span para la palabra
                const wordSpan = document.createElement("span");
                wordSpan.title = word;
                // Aplicar estilos para truncar texto largo
                if (searchTerm)
                {
                    const i = word.toLowerCase().indexOf(searchTerm);
                    if (i !== -1)
                    {
                        const before = word.substring(0, i);
                        const match = word.substring(i, i + searchTerm.length);
                        const after = word.substring(i + searchTerm.length);
                        wordSpan.innerHTML = `${before}<mark>${match}</mark>${after}`;
                    }
                    else
                    {
                        wordSpan.textContent = word;
                    }
                }
                else
                {
                    wordSpan.textContent = word;
                }
                // Estilos para el span de la palabra
                const btnContainer = document.createElement("span");
                btnContainer.style.display = "flex";
                btnContainer.style.gap = "4px";
                // Botón Editar
                const editBtn = document.createElement("button");
                editBtn.innerHTML = "✏️";
                editBtn.title = "Editar";
                editBtn.style.border = "none";
                editBtn.style.background = "transparent";
                editBtn.style.cursor = "pointer";
                editBtn.style.padding = "2px";
                editBtn.style.fontSize = "14px";
                editBtn.addEventListener("click", () => {
                    const newWord = prompt("Editar palabra:", word);
                    if (newWord !== null && newWord.trim() !== word)
                    { // Permitir string vacío para borrar si se quisiera, pero
                        const trimmedNewWord = newWord.trim();
                        if (trimmedNewWord === "")
                        {
                            alert("La palabra no puede estar vacía.");
                            return;
                        }
                        if (window.swapWords.includes(trimmedNewWord) && trimmedNewWord !== word)
                        {
                            alert("Esa palabra ya existe en la lista.");
                            return;
                        }
                        window.swapWords = window.swapWords.filter(w => w !== word);
                        window.swapWords.push(trimmedNewWord);
                        saveSwapWordsToStorage();
                        renderSwapList(searchInput);
                    }
                });
                // Botón Eliminar
                const deleteBtn = document.createElement("button");
                deleteBtn.innerHTML = "🗑️";
                deleteBtn.title = "Eliminar";
                deleteBtn.style.border = "none";
                deleteBtn.style.background = "transparent";
                deleteBtn.style.cursor = "pointer";
                deleteBtn.style.padding = "2px";
                deleteBtn.style.fontSize = "14px";
                deleteBtn.addEventListener("click", () => {
                    if (confirm(`¿Eliminar la palabra '${word}' de la lista?`))
                    {
                        window.swapWords = window.swapWords.filter(w => w !== word);
                        renderSwapList(searchInput);
                        saveSwapWordsToStorage();
                    }
                });
                btnContainer.appendChild(editBtn);
                btnContainer.appendChild(deleteBtn);
                li.appendChild(wordSpan);
                li.appendChild(btnContainer);
                swapList.appendChild(li);
            });
        }
        // Render inicial
        renderReplacementsList(listElement);
        if (window.swapWords && window.swapWords.size > 0) renderSwapList();
        // Listener de búsqueda para swap
        searchSwapInput.addEventListener("input", renderSwapList);
    }
    //Renderizar lista de palabras excluidas
    function renderExcludedWordsList(ulElement, filter = "")
    { 
        // Asegurarse de que ulElement es válido
        if (!ulElement)
        {
            // Intentar obtenerlo por ID como último recurso si no se pasó,
            // pero idealmente siempre se pasa desde el llamador.
            ulElement = document.getElementById("excludedWordsList");
            if (!ulElement)
            {
                console.error("[WME PLN] Contenedor 'excludedWordsList' no encontrado para renderizar.");
                return;
            }
        }
        // Asegurarse de que excludedWords es un Set
        const currentFilter = filter.toLowerCase();       
        ulElement.innerHTML = ""; // Limpiar lista anterior
        // Asegurarse de que excludedWords es un Set
        const wordsToRender =
        Array.from(excludedWords)
            .filter(word => word.toLowerCase().includes(currentFilter))
            .sort((a, b) => a.toLowerCase().localeCompare(
                    b.toLowerCase())); // Ordenar alfabéticamente

        if (wordsToRender.length === 0)
        {
            const li = document.createElement("li");
            li.style.padding = "5px";
            li.style.textAlign = "center";
            li.style.color = "#777";
            if (excludedWords.size === 0)
            {
                li.textContent = "La lista está vacía.";
            }
            else if (currentFilter !== "")
            {
                li.textContent = "No hay coincidencias para el filtro.";
            }
            else
            {
                li.textContent = "La lista está vacía (o error inesperado)."; // Fallback
            }
            ulElement.appendChild(li);
        }
        else
        {
            wordsToRender.forEach(word => {
                const li = document.createElement("li");
                li.style.display = "flex";
                li.style.justifyContent = "space-between";
                li.style.alignItems = "center";
                li.style.padding = "4px 2px"; // Ajuste
                li.style.borderBottom = "1px solid #f0f0f0";
                // Span para la palabra
                const wordSpan = document.createElement("span");
                wordSpan.textContent = word;
                wordSpan.style.maxWidth = "calc(100% - 60px)"; // Dejar espacio para botones
                wordSpan.style.overflow = "hidden";
                wordSpan.style.textOverflow = "ellipsis";
                wordSpan.style.whiteSpace = "nowrap";
                wordSpan.title = word;
                li.appendChild(wordSpan);
                // Contenedor para los iconos de acción
                const iconContainer = document.createElement("span");
                iconContainer.style.display = "flex";
                iconContainer.style.gap = "8px"; // Más espacio entre iconos
                // Botón de edición
                const editBtn = document.createElement("button");
                editBtn.innerHTML = "✏️";
                editBtn.title = "Editar";
                editBtn.style.border = "none";
                editBtn.style.background = "transparent";
                editBtn.style.cursor = "pointer";
                editBtn.style.padding = "2px";
                editBtn.style.fontSize = "14px"; // Iconos un poco más grandes
                // Añadir evento de edición
                editBtn.addEventListener("click", () => {
                    const newWord = prompt("Editar palabra:", word);
                    if (newWord !== null && newWord.trim() !== word)
                    { // Permitir string vacío para borrar si se quisiera, pero
                        // trim() lo evita
                        const trimmedNewWord = newWord.trim();
                        if (trimmedNewWord === "")
                        {
                            alert("La palabra no puede estar vacía.");
                            return;
                        }
                        if (excludedWords.has(trimmedNewWord) &&
                            trimmedNewWord !== word)
                        {
                            alert("Esa palabra ya existe en la lista.");
                            return;
                        }
                        excludedWords.delete(word);
                        // Añadir al mapa de palabras excluidas
                        const firstCharDeleted = word.charAt(0).toLowerCase();
                        if (excludedWordsMap.has(firstCharDeleted))
                        {
                            excludedWordsMap.get(firstCharDeleted).delete(word);
                            if (excludedWordsMap.get(firstCharDeleted).size === 0) {
                                excludedWordsMap.delete(firstCharDeleted); // Eliminar la clave si el Set está vacío
                            }
                        }
                        excludedWords.add(trimmedNewWord);
                        renderExcludedWordsList(ulElement, currentFilter);
                    }
                });
                const deleteBtn = document.createElement("button");
                deleteBtn.innerHTML = "🗑️";
                deleteBtn.title = "Eliminar";
                deleteBtn.style.border = "none";
                deleteBtn.style.background = "transparent";
                deleteBtn.style.cursor = "pointer";
                deleteBtn.style.padding = "2px";
                deleteBtn.style.fontSize = "14px";
                deleteBtn.addEventListener("click", () => {
                    if (confirm(`¿Estás seguro de que deseas eliminar la palabra '${
                        word}'?`))
                    {
                        excludedWords.delete(word);
                        // Añadir al mapa de palabras excluidas
                        const firstCharDeleted = word.charAt(0).toLowerCase();
                        if (excludedWordsMap.has(firstCharDeleted))
                        {
                            excludedWordsMap.get(firstCharDeleted).delete(word);
                            if (excludedWordsMap.get(firstCharDeleted).size === 0) {
                                excludedWordsMap.delete(firstCharDeleted); // Eliminar la clave si el Set está vacío
                            }
                        }
                        renderExcludedWordsList(ulElement, currentFilter);
                    }
                });
                iconContainer.appendChild(editBtn);
                iconContainer.appendChild(deleteBtn);
                li.appendChild(iconContainer);
                ulElement.appendChild(li);
            });
        }
        // Guardar la lista actualizada en localStorage después de cada render
        try
        {
            localStorage.setItem("excludedWordsList", JSON.stringify(Array.from(excludedWords)));             
        }
        catch (e)
        {
            console.error("[WME PLN] Error guardando en localStorage:", e);
            // Considerar no alertar cada vez para no ser molesto si el localStorage está lleno. Podría ser un mensaje en consola o una notificación sutil en la UI.
        }
    }// renderExcludedWordsList

    // Renderizar lista de palabras del diccionario
    function renderDictionaryList(ulElement, filter = "")
    {
        // Asegurarse de que ulElement es válido
        if (!ulElement || !window.dictionaryWords)
            return;
        // Asegurarse de que ulElement es válido
        const currentFilter = filter.toLowerCase();
        ulElement.innerHTML = "";
        // Asegurarse de que dictionaryWords es un Set
        const wordsToRender =
        Array.from(window.dictionaryWords)
            .filter(word => word.toLowerCase().startsWith(currentFilter))
            .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
        // Si no hay palabras que renderizar, mostrar mensaje
        if (wordsToRender.length === 0)
        {
            const li = document.createElement("li");
            li.textContent = window.dictionaryWords.size === 0
                            ? "El diccionario está vacío."
                            : "No hay coincidencias.";
            li.style.textAlign = "center";
            li.style.color = "#777";
            ulElement.appendChild(li);
            // Guardar diccionario también cuando está vacío
            try
            {
                localStorage.setItem(
                "dictionaryWordsList",
                JSON.stringify(Array.from(window.dictionaryWords)));
            }
            catch (e)
            {
                console.error( "[WME PLN] Error guardando el diccionario en localStorage:", e);
            }
            return;
        }
        // Renderizar cada palabra
        wordsToRender.forEach(word => {
            const li = document.createElement("li");
            li.style.display = "flex";
            li.style.justifyContent = "space-between";
            li.style.alignItems = "center";
            li.style.padding = "4px 2px";
            li.style.borderBottom = "1px solid #f0f0f0";
            // Span para la palabra
            const wordSpan = document.createElement("span");
            wordSpan.textContent = word;
            wordSpan.style.maxWidth = "calc(100% - 60px)";
            wordSpan.style.overflow = "hidden";
            wordSpan.style.textOverflow = "ellipsis";
            wordSpan.style.whiteSpace = "nowrap";
            wordSpan.title = word;
            li.appendChild(wordSpan);
            // Contenedor para los iconos de acción
            const iconContainer = document.createElement("span");
            iconContainer.style.display = "flex";
            iconContainer.style.gap = "8px";
            // Botón de edición y eliminación
            const editBtn = document.createElement("button");
            editBtn.innerHTML = "✏️";
            editBtn.title = "Editar";
            editBtn.style.border = "none";
            editBtn.style.background = "transparent";
            editBtn.style.cursor = "pointer";
            editBtn.style.padding = "2px";
            editBtn.style.fontSize = "14px";
            editBtn.addEventListener("click", () => {
                const newWord = prompt("Editar palabra:", word);
                if (newWord !== null && newWord.trim() !== word)
                {
                    window.dictionaryWords.delete(word);
                    window.dictionaryWords.add(newWord.trim());
                    renderDictionaryList(ulElement, currentFilter);
                }
            });
            // Botón de eliminación
            const deleteBtn = document.createElement("button");
            deleteBtn.innerHTML = "🗑️";
            deleteBtn.title = "Eliminar";
            deleteBtn.style.border = "none";
            deleteBtn.style.background = "transparent";
            deleteBtn.style.cursor = "pointer";
            deleteBtn.style.padding = "2px";
            deleteBtn.style.fontSize = "14px";
            deleteBtn.addEventListener("click", () => {
                // Confirmación antes de eliminar
                if (confirm(`¿Eliminar la palabra '${word}' del diccionario?`))
                {
                    window.dictionaryWords.delete(word);
                    renderDictionaryList(ulElement, currentFilter);
                }
            });
            iconContainer.appendChild(editBtn);
            iconContainer.appendChild(deleteBtn);
            li.appendChild(iconContainer);
            ulElement.appendChild(li);
        });
        // Guardar el diccionario actualizado en localStorage después de cada render
        try
        {
            localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
        }
        catch (e)
        {
            console.error("[WME PLN] Error guardando el diccionario en localStorage:", e);
        }
    }
        // Función para manejar el archivo XML arrastrado    
        function exportExcludedWordsList()
        {   
            // Verificar si hay palabras excluidas
            if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
            {
                alert("No hay palabras especiales ni reemplazos para exportar.");
                return;
            }
            // Crear el contenido XML
            let xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<ExcludedWords>\n`;
            xmlContent +=
            Array.from(excludedWords)
                .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
                .map(w => `    <word>${xmlEscape(w)}</word>`)
                    .join("\n");
            // Añadir reemplazos si existen
            if (Object.keys(replacementWords).length > 0)
            {
                xmlContent += "\n";
                xmlContent +=
                Object.entries(replacementWords)
                    .map(([ from, to ]) => `    <replacement from="${
                        xmlEscape(from)}">${xmlEscape(to)}</replacement>`)
                    .join("\n");
            }
            xmlContent += "\n</ExcludedWords>";
            // Crear el Blob y descargarlo
            const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
            // Crear un enlace temporal para descargar el archivo
            const url = URL.createObjectURL(blob);
            // Crear un elemento <a> para descargar el archivo
            const a = document.createElement("a");
            a.href = url;
            a.download = "wme_excluded_words_export.xml";
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
        // Función para exportar palabras del diccionario a XML
        function exportDictionaryWordsList()
        {
            // Verificar si hay palabras en el diccionario
            if (window.dictionaryWords.size === 0)
            {
                alert(
                "La lista de palabras del diccionario está vacía. Nada que exportar.");
                return;
            }
            // Crear el contenido XML
            const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<diccionario>\n${
                Array.from(window.dictionaryWords)
                .sort((a, b) => a.toLowerCase().localeCompare(
                        b.toLowerCase()))                     // Exportar ordenado
                .map(w => `    <word>${xmlEscape(w)}</word>`) // Indentación y escape
                .join("\n")}\n</diccionario>`;
            // Crear el Blob y descargarlo
            const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" }); // Añadir charset
            // Crear un enlace temporal para descargar el archivo
            const url = URL.createObjectURL(blob);
            // Crear un elemento <a> para descargar el archivo
            const a = document.createElement("a");
            a.href = url;
            a.download = "wme_dictionary_words_export.xml"; // Nombre más descriptivo
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
        // Función para exportar datos compartidos a XML
        function xmlEscape(str)
        {
            return str.replace(/[<>&"']/g, function(match) {
                switch (match)
                {
                    case '<':
                        return '&lt;';
                    case '>':
                        return '&gt;';
                    case '&':
                        return '&amp;';
                    case '"':
                        return '&quot;';
                    case "'":
                        return '&apos;';
                    default:
                        return match;
                }
            });
        }
        // Función para manejar el archivo XML arrastrado
        waitForSidebarAPI();
        // Obtener información del usuario logueado
        const currentUser = (wmeSDK?.DataModel?.getLoggedInUser?.()) || (W?.loginManager?.user) || null;
        if (currentUser)
        {
            console.log("Usuario Logueado ID:", currentUser.userId);
            console.log("Usuario Logueado Nombre:", currentUser.userName);    
        }
        else
        {
            console.log("No se pudo determinar el usuario logueado.");
        }
        })();
        // Función reutilizable para mostrar el spinner de carga
        function showLoadingSpinner()
        {
            const scanSpinner = document.createElement("div");
            scanSpinner.id = "scanSpinnerOverlay";
            scanSpinner.style.position = "fixed";
            scanSpinner.style.top = "0";
            scanSpinner.style.left = "0";
            scanSpinner.style.width = "100%";
            scanSpinner.style.height = "100%";
            scanSpinner.style.background = "rgba(0, 0, 0, 0.5)";
            scanSpinner.style.zIndex = "10000";
            scanSpinner.style.display = "flex";
            scanSpinner.style.justifyContent = "center";
            scanSpinner.style.alignItems = "center";

            const scanContent = document.createElement("div");
            scanContent.style.background = "#fff";
            scanContent.style.padding = "20px";
            scanContent.style.borderRadius = "8px";
            scanContent.style.textAlign = "center";

            const spinner = document.createElement("div");
            spinner.classList.add("spinner");
            spinner.style.border = "6px solid #f3f3f3";
            spinner.style.borderTop = "6px solid #3498db";
            spinner.style.borderRadius = "50%";
            spinner.style.width = "40px";
            spinner.style.height = "40px";
            spinner.style.animation = "spin 1s linear infinite";
            spinner.style.margin = "0 auto 10px auto";

            const progressText = document.createElement("div");
            progressText.id = "scanProgressText";
            progressText.textContent = "Analizando lugares: 0%";
            progressText.style.fontSize = "14px";
            progressText.style.color = "#333";

            scanContent.appendChild(spinner);
            scanContent.appendChild(progressText);
            scanSpinner.appendChild(scanContent);
            document.body.appendChild(scanSpinner);

            const style = document.createElement("style");
            style.textContent = `
                            @keyframes spin {
                            0% { transform: rotate(0deg); }
                            100% { transform: rotate(360deg); }
                            }
                        `;
            document.head.appendChild(style);
        }
        // Función para obtener el ícono de categoría    
        function getCategoryIcon(categoryName)
        {
            // Mapa de categorías a íconos con soporte bilingüe
            const categoryIcons = {
                // Comida y Restaurantes / Food & Restaurants
                "FOOD_AND_DRINK": { icon: "🦞🍷", es: "Comida y Bebidas", en: "Food and Drinks" },
                "RESTAURANT": { icon: "🍽️", es: "Restaurante", en: "Restaurant" },
                "FAST_FOOD": { icon: "🍔", es: "Comida rápida", en: "Fast Food" },
                "CAFE": { icon: "☕", es: "Cafetería", en: "Cafe" },
                "BAR": { icon: "🍺", es: "Bar", en: "Bar" },
                "BAKERY": { icon: "🥖", es: "Panadería", en: "Bakery" },
                "ICE_CREAM": { icon: "🍦", es: "Heladería", en: "Ice Cream Shop" },
                "DEPARTMENT_STORE": { icon: "🏬", es: "Tienda por departamentos", en: "Department Store" },

                "PARK": { icon: "🌳", es: "Parque", en: "Park" },

                // Compras y Servicios / Shopping & Services
                "FASHION_AND_CLOTHING": { icon: "👗", es: "Moda y Ropa", en: "Fashion and Clothing" },
                "SHOPPING_AND_SERVICES": { icon: "👜👝", es: "Mercado o Tienda", en: "Shopping and Services" },
                "SHOPPING_CENTER": { icon: "🛍️", es: "Centro comercial", en: "Shopping Center" },
                "SUPERMARKET_GROCERY": { icon: "🛒", es: "Supermercado", en: "Supermarket" },
                "MARKET": { icon: "🛒", es: "Mercado", en: "Market" },
                "CONVENIENCE_STORE": { icon: "🏪", es: "Tienda", en: "Convenience Store" },
                "PHARMACY": { icon: "💊", es: "Farmacia", en: "Pharmacy" },
                "BANK": { icon: "🏦", es: "Banco", en: "Bank" },
                "ATM": { icon: "💳", es: "Cajero automático", en: "ATM" },
                "HARDWARE_STORE": { icon: "🔧", es: "Ferretería", en: "Hardware Store" },
                "COURTHOUSE": { icon: "⚖️", es: "Corte", en: "Courthouse" },
                "FURNITURE_HOME_STORE": { icon: "🛋️", es: "Tienda de muebles", en: "Furniture Store" },
                "TOURIST_ATTRACTION_HISTORIC_SITE": { icon: "🗿", es: "Atracción turística o Sitio histórico", en: "Tourist Attraction or Historic Site" },
                "PET_STORE_VETERINARIAN_SERVICES": { icon: "🦮🐈", es: "Tienda de mascotas o Veterinaria", en: "Pet Store or Veterinary Services" },
                "CEMETERY": { icon: "🪦", es: "Cementerio", en: "Cemetery" },
                "KINDERGARDEN": { icon: "🍼", es: "Jardín Infantil", en: "Kindergarten" },
                "JUNCTION_INTERCHANGE": { icon: "🔀", es: "Cruce o Intercambio", en: "Junction or Interchange" },
                "OUTDOORS": { icon: "🏞️", es: "Aire libre", en: "Outdoors" },
                "ORGANIZATION_OR_ASSOCIATION": { icon: "👔", es: "Organización o Asociación", en: "Organization or Association" },
                "TRAVEL_AGENCY": { icon: "🧳", es: "Agencia de viajes", en: "Travel Agency" },
                "BANK_FINANCIAL": { icon: "💰", es: "Banco o Financiera", en: "Bank or Financial Institution" },
                "SPORTING_GOODS": { icon: "🛼🏀🏐", es: "Artículos deportivos", en: "Sporting Goods" },
                "TOY_STORE": { icon: "🧸", es: "Tienda de juguetes", en: "Toy Store" },
                "CURRENCY_EXCHANGE": { icon: "💶💱", es: "Casa de cambio", en: "Currency Exchange" },
                "PHOTOGRAPHY": { icon: "📸", es: "Fotografía", en: "Photography" },
                "DESSERT": { icon: "🍰", es: "Postre", en: "Dessert" },
                "FOOD_COURT": { icon: "🥗", es: "Comedor o Patio de comidas", en: "Food Court" },
                "CANAL": { icon: "〰", es: "Canal", en: "Canal" },
                "JEWELRY": { icon: "💍", es: "Joyería", en: "Jewelry" },

                // Transporte / Transportation
                "TRAIN_STATION": { icon: "🚂", es: "Estación de tren", en: "Train Station" },
                "GAS_STATION": { icon: "⛽", es: "Estación de servicio", en: "Gas Station" },
                "PARKING_LOT": { icon: "🅿️", es: "Estacionamiento", en: "Parking Lot" },
                "BUS_STATION": { icon: "🚍", es: "Terminal de bus", en: "Bus Station" },
                "AIRPORT": { icon: "✈️", es: "Aeropuerto", en: "Airport" },
                "CAR_WASH": { icon: "🚗💦", es: "Lavado de autos", en: "Car Wash" },
                "TAXI_STATION": { icon: "🚕", es: "Estación de taxis", en: "Taxi Station" },
                "FOREST_GROVE": { icon: "🌳", es: "Bosque", en: "Forest Grove" },
                "GARAGE_AUTOMOTIVE_SHOP": { icon: "🔧🚗", es: "Taller mecánico", en: "Automotive Garage" },
                "GIFTS": { icon: "🎁", es: "Tienda de regalos", en: "Gift Shop" },
                "TOLL_BOOTH": { icon: "🚧", es: "Peaje", en: "Toll Booth" },
                "CHARGING_STATION": { icon: "🔋", es: "Estación de carga", en: "Charging Station" },
                "CAR_SERVICES": { icon: "🚗🔧", es: "Servicios de automóviles", en: "Car Services" },
                "STADIUM_ARENA": { icon: "🏟️", es: "Estadio o Arena", en: "Stadium or Arena" },
                "CAR_DEALERSHIP": { icon: "🚘🏢", es: "Concesionario de autos", en: "Car Dealership" },
                "FERRY_PIER": { icon: "⛴️", es: "Muelle de ferry", en: "Ferry Pier" },
                "INFORMATION_POINT": { icon: "ℹ️", es: "Punto de información", en: "Information Point" },
                "REST_AREAS": { icon: "🏜", es: "Áreas de descanso", en: "Rest Areas" },
                "MUSIC_VENUE": { icon: "🎶", es: "Lugar de música", en: "Music Venue" },
                "CASINO": { icon: "🎰", es: "Casino", en: "Casino" },
                "CITY_HALL": { icon: "🎩", es: "Ayuntamiento", en: "City Hall" },
                "PERFORMING_ARTS_VENUE": { icon: "🎭", es: "Lugar de artes escénicas", en: "Performing Arts Venue" },
                "TUNNEL": { icon: "🔳", es: "Túnel", en: "Tunnel" },
                "SEAPORT_MARINA_HARBOR": { icon: "⚓", es: "Puerto o Marina", en: "Seaport or Marina" },


                // Alojamiento / Lodging
                "HOTEL": { icon: "🏨", es: "Hotel", en: "Hotel" },
                "HOSTEL": { icon: "🛏️", es: "Hostal", en: "Hostel" },
                "LODGING": { icon: "⛺", es: "Alojamiento", en: "Lodging" },
                "MOTEL": { icon: "🛕", es: "Motel", en: "Motel" },
                "SWIMMING_POOL": { icon: "🏊", es: "Piscina", en: "Swimming Pool" },
                "RIVER_STREAM": { icon: "🌊", es: "Río o Arroyo", en: "River or Stream" },
                "CAMPING_TRAILER_PARK": { icon: "🏕️", es: "Camping o Parque de Trailers", en: "Camping or Trailer Park" },
                "SEA_LAKE_POOL": { icon: "🏖️", es: "Mar, Lago o Piscina", en: "Sea, Lake or Pool" },
                "FARM": { icon: "🚜", es: "Granja", en: "Farm" },
                "NATURAL_FEATURES": { icon: "🌲", es: "Características naturales", en: "Natural Features" },

                // Salud / Healthcare
                "HOSPITAL": { icon: "🏥", es: "Hospital", en: "Hospital" },
                "HOSPITAL_URGENT_CARE": { icon: "🏥🚑", es: "Urgencias", en: "Urgent Care" },
                "DOCTOR_CLINIC": { icon: "🏥⚕️", es: "Clínica", en: "Clinic" },
                "DOCTOR": { icon: "👨‍⚕️", es: "Consultorio médico", en: "Doctor's Office" },
                "VETERINARY": { icon: "🐾", es: "Veterinaria", en: "Veterinary" },
                "PERSONAL_CARE": { icon: "💅💇🦷", es: "Cuidado personal", en: "Personal Care" },
                "FACTORY_INDUSTRIAL": { icon: "🏭", es: "Fábrica o Industrial", en: "Factory or Industrial" },
                "MILITARY": { icon: "🪖", es: "Militar", en: "Military" },
                "LAUNDRY_DRY_CLEAN": { icon: "🧺", es: "Lavandería o Tintorería", en: "Laundry or Dry Clean" },
                "PLAYGROUND": { icon: "🛝", es: "Parque infantil", en: "Playground" },
                "TRASH_AND_RECYCLING_FACILITIES": { icon: "🗑️♻️", es: "Instalaciones de basura y reciclaje", en: "Trash and Recycling Facilities" },

                // Educación / Education
                "UNIVERSITY": { icon: "🎓", es: "Universidad", en: "University" },
                "COLLEGE_UNIVERSITY": { icon: "🏫", es: "Colegio", en: "College" },
                "SCHOOL": { icon: "🎒", es: "Escuela", en: "School" },
                "LIBRARY": { icon: "📖", es: "Biblioteca", en: "Library" },
                "FLOWERS": { icon: "💐", es: "Floristería", en: "Flower Shop" },
                "CONVENTIONS_EVENT_CENTER": { icon: "🎤🥂", es: "Centro de convenciones o eventos", en: "Convention or Event Center" },
                "CLUB": { icon: "♣", es: "Club", en: "Club" },
                "ART_GALLERY": { icon: "🖼️", es: "Galería de arte", en: "Art Gallery" },
                "NATURAL_FEATURES": { icon: "🌄", es: "Características naturales", en: "Natural Features" },

                // Entretenimiento / Entertainment
                "CINEMA": { icon: "🎬", es: "Cine", en: "Cinema" },
                "THEATER": { icon: "🎭", es: "Teatro", en: "Theater" },
                "MUSEUM": { icon: "🖼", es: "Museo", en: "Museum" },
                "CULTURE_AND_ENTERTAINEMENT": { icon: "🎨", es: "Cultura y Entretenimiento", en: "Culture and Entertainment" },
                "STADIUM": { icon: "🏟️", es: "Estadio", en: "Stadium" },
                "GYM": { icon: "💪", es: "Gimnasio", en: "Gym" },
                "GYM_FITNESS": { icon: "🏋️", es: "Gimnasio o Fitness", en: "Gym or Fitness" },
                "GAME_CLUB": { icon: "⚽🏓", es: "Club de juegos", en: "Game Club" },
                "BOOKSTORE": { icon: "📖📚", es: "Librería", en: "Bookstore" },
                "ELECTRONICS": { icon: "📱💻", es: "Electrónica", en: "Electronics" },
                "SPORTS_COURT": { icon: "⚽🏀", es: "Cancha deportiva", en: "Sports Court" },
                "GOLF_COURSE": { icon: "⛳", es: "Campo de golf", en: "Golf Course" },
                "SKI_AREA": { icon: "⛷️", es: "Área de esquí", en: "Ski Area" },
                "RACING_TRACK": { icon: "🛷⛸🏎️", es: "Pista de carreras", en: "Racing Track" },

                // Gobierno y Servicios Públicos / Government & Public Services
                "GOVERNMENT": { icon: "🏛️", es: "Oficina gubernamental", en: "Government Office" },
                "POLICE_STATION": { icon: "👮", es: "Estación de policía", en: "Police Station" },
                "FIRE_STATION": { icon: "🚒", es: "Estación de bomberos", en: "Fire Station" },
                "FIRE_DEPARTMENT": { icon: "🚒", es: "Departamento de bomberos", en: "Fire Department" },
                "POST_OFFICE": { icon: "📫", es: "Correo", en: "Post Office" },
                "TRANSPORTATION": { icon: "🚌", es: "Transporte", en: "Transportation" },
                "PRISON_CORRECTIONAL_FACILITY": { icon: "👁️‍🗨️", es: "Prisión o Centro Correccional", en: "Prison or Correctional Facility" },

                // Religión / Religion
                "RELIGIOUS_CENTER": { icon: "⛪", es: "Iglesia", en: "Church" },

                // Otros / Others
                "RESIDENTIAL": { icon: "🏘️", es: "Residencial", en: "Residential" },
                "RESIDENCE_HOME": { icon: "🏠", es: "Residencia o Hogar", en: "Residence or Home" },
                "OFFICES": { icon: "🏢", es: "Oficina", en: "Office" },
                "FACTORY": { icon: "🏭", es: "Fábrica", en: "Factory" },
                "CONSTRUCTION_SITE": { icon: "🏗️", es: "Construcción", en: "Construction" },
                "MONUMENT": { icon: "🗽", es: "Monumento", en: "Monument" },
                "BRIDGE": { icon: "🌉", es: "Puente", en: "Bridge" },
                "PROFESSIONAL_AND_PUBLIC": { icon: "🗄💼", es: "Profesional y Público", en: "Professional and Public" },
                "OTHER": { icon: "🚪", es: "Otro", en: "Other" },
                "ARTS_AND_CRAFTS": { icon: "🎨", es: "Artes y Manualidades", en: "Arts and Crafts" },
                "COTTAGE_CABIN": { icon: "🏡", es: "Cabaña", en: "Cottage Cabin" },
                "TELECOM": { icon: "📡", es: "Telecomunicaciones", en: "Telecommunications" }
            };
            // Si no hay categoría, devolver ícono por defecto
            if (!categoryName) {
                return { icon: "❓", title: "Sin categoría / No category" };
            }
            // Normalizar el nombre de la categoría
            const normalizedInput = categoryName.toLowerCase()
                .normalize("NFD")
                .replace(/[\u0300-\u036f]/g, "")
                .trim();

//console.log("[WME PLN DEBUG] Buscando ícono para categoría:", categoryName);
//console.log("[WME PLN DEBUG] Nombre normalizado:", normalizedInput);

            // 1. Buscar coincidencia exacta por clave interna (ej: "PARK")
            for (const [key, data] of Object.entries(categoryIcons)) {
                if (key.toLowerCase() === normalizedInput) {
                    return { icon: data.icon, title: `${data.es} / ${data.en}` };
                }
            }
            // Buscar coincidencia en el mapa de categorías
            for (const [key, data] of Object.entries(categoryIcons))
            {
                // Normalizar los nombres en español e inglés para la comparación
                const normalizedES = data.es.toLowerCase()
                    .normalize("NFD")
                    .replace(/[\u0300-\u036f]/g, "")
                    .trim();
                const normalizedEN = data.en.toLowerCase()
                    .normalize("NFD")
                    .replace(/[\u0300-\u036f]/g, "")
                    .trim();
                if (normalizedInput === normalizedES || normalizedInput === normalizedEN)
                {
                    return { icon: data.icon, title: `${data.es} / ${data.en}` };
                }
            }
            // Si no se encuentra coincidencia, devolver ícono por defecto
//console.log("[WME PLN DEBUG] No se encontró coincidencia, usando ícono por defecto");
            return {
                icon: "⚪",
                title: `${categoryName} (Sin coincidencia / No match)`
            };
        }// getCategoryIcon
        // Función para agregar una palabra al diccionario
        function addWordToDictionary(input)
        {
            const newWord = input.value.trim().toLowerCase();

            if (!newWord)
            {
                alert("La palabra no puede estar vacía.");
                return;
            }
            // Validaciones básicas antes de añadir
            if (newWord.length === 1 && !newWord.match(/[a-zA-Z0-9]/)) {
                alert("No se permite agregar un solo carácter que no sea alfanumérico.");
                return;
            }
            if (commonWords.includes(newWord)) {
                alert("Esa palabra es muy común y no debe agregarse al diccionario.");
                return;
            }
            if (excludedWords.has(newWord)) {
                alert("Esa palabra ya existe en la lista de especiales (excluidas).");
                return;
            }
            if (window.dictionaryWords.has(newWord)) {
                alert("La palabra ya existe en el diccionario.");
                return;
            }
            if (!window.dictionaryWords) window.dictionaryWords = new Set();
            if (!window.dictionaryIndex) window.dictionaryIndex = {};
            window.dictionaryWords.add(newWord); // Añadir al Set
            // === AÑADIR AL ÍNDICE ===
            const firstChar = newWord.charAt(0).toLowerCase();
            if (!window.dictionaryIndex[firstChar]) {
                window.dictionaryIndex[firstChar] = [];
            }
            window.dictionaryIndex[firstChar].push(newWord); // Añadir al índice
            input.value = ""; // Limpiar el input
            renderDictionaryList(document.getElementById("dictionaryWordsList")); // Re-renderizar la lista
            // Guardar en localStorage después de añadir
            try
            {
                localStorage.setItem("dictionaryWordsList", JSON.stringify(Array.from(window.dictionaryWords)));
            }
            catch (e)
            {
                console.error("[WME PLN] Error guardando diccionario en localStorage después de añadir manualmente:", e);
            }
        }// addWordToDictionary
    

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址