WME Places Name Normalizer

Normaliza nombres de lugares en Waze Map Editor (WME)

// ==UserScript==
// @name         WME Places Name Normalizer
// @namespace    https://gf.qytechs.cn/en/users/mincho77
// @version      6.2.2
// @author       Mincho77
// @description  Normaliza nombres de lugares en Waze Map Editor (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        none
// @run-at       document-end
// ==/UserScript==
(function() {
    // Variables globales básicas
        const SCRIPT_NAME = GM_info.script.name;
        const VERSION = GM_info.script.version.toString();

    // Variables globales para el panel flotante
    let floatingPanelElement = null;
    const processingPanelDimensions = { width: '400px', height: '200px' };  // Panel pequeño para procesamiento
    const resultsPanelDimensions = { width: '1300px', height: '700px' };    // Panel grande para resultados

        const commonWords = [
            'de',  'del', 'el',    'la',   'los',  'las',  'y', 'e',
            'o',   'u',   'un',    'una',  'unos', 'unas', 'a', 'en',
            'con', 'sin', 'sobre', '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.

        //****************************************************************************************************
        // Nombre: tryInitializeSDK
        // Autor: mincho77
        // Fecha: 2025-05-27
        // Descripción: Intenta inicializar el SDK de WME de forma asíncrona.
        // Parámetros:
        //   finalCallback (Function): Función a llamar después del intento de inicialización (exitoso o no).
        //****************************************************************************************************
        function tryInitializeSDK(finalCallback)
        {
            let attempts = 0;
            const maxAttempts = 60; // Intentos máximos (60 * 500ms = 30 segundos)
            const intervalTime = 500;
            let sdkAttemptInterval = null;

            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();
        }
        //****************************************************************************************************

    function waitForWazeAPI(callbackPrincipalDelScript)
    {
        let wAttempts = 0;
        const wMaxAttempts = 40;
        const wInterval = setInterval(() => {
            wAttempts++;
            if (typeof W !== 'undefined' && W.map && W.loginManager && W.model &&
                W.model.venues && W.userscripts &&
                typeof W.userscripts.registerSidebarTab === 'function')
            {
                clearInterval(wInterval);
               // console.log("✅ Waze API (objeto W) cargado correctamente.");
                tryInitializeSDK(callbackPrincipalDelScript);
            }
            else if (wAttempts >= wMaxAttempts)
            {
                clearInterval(wInterval);
              //  console.error("Error: No se pudo cargar la API principal de Waze (objeto W) después de varios intentos.");
                callbackPrincipalDelScript();
            }
        }, 500);
    }


    function updateScanProgressBar(currentIndex, totalPlaces)
    {
        if (totalPlaces === 0)
        {
            return;
        }

        let progressPercent = Math.floor(((currentIndex + 1) / totalPlaces) * 100);
        progressPercent = Math.min(progressPercent, 100);

        const progressBarInnerTab = document.getElementById("progressBarInnerTab");
        const progressBarTextTab = document.getElementById("progressBarTextTab");

        if (progressBarInnerTab && progressBarTextTab)
        {
            progressBarInnerTab.style.width = `${progressPercent}%`;
            const currentDisplay = Math.min(currentIndex + 1, totalPlaces);
            progressBarTextTab.textContent =
              `Progreso: ${progressPercent}% (${currentDisplay}/${totalPlaces})`;
        }
    }

    // Función para escapar caracteres especiales en expresiones regulares
    function escapeRegExp(string)
    {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // Función para eliminar tildes/diacríticos de una cadena
    function removeDiacritics(str)
    {
        return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
    }
    // Función para calcular la similitud entre dos cadenas
    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 };
    }

    // Función para calcular la similitud entre dos cadenas
    function aplicarReemplazosGenerales(name)
    {
        if (typeof window.skipGeneralReplacements === "boolean" &&
            window.skipGeneralReplacements)
        {
            return 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 : " / " },
            { buscar : /\[P\]|\[p\]/g, reemplazar : "" },
            { buscar : /\s*-\s*/g, reemplazar : " - " },
        ];
        reglas.forEach(
          regla => { name = name.replace(regla.buscar, regla.reemplazar); });
        name = name.replace(/\s{2,}/g, ' ');
        return name;
    }

    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()}`);
        // --- INICIO: NUEVA REGLA PARA 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();
            }
        );
        // --- FIN: NUEVA REGLA ---
        const frasesAlFinal = [
            "Conjunto Residencial",
            "Urbanización",
            "Conjunto Cerrado",
            "Unidad Residencial",
            "Parcelación",
            "Condominio Campestre",
            "Condominio",
            "Edificio",
            "Conjunto Habitacional",
            "Apartamentos",
            "Club Campestre",
            "Club Residencial",
            "Motel",
            "Restaurante",
            "Eco Hotel",
            "Finca Hotel",
            "Hotel"
        ];

        for (const frase of frasesAlFinal)
        {
            const regex = new RegExp(`\\s+${escapeRegExp(frase)}$`, 'i');
            if (regex.test(newName))
            {
                const match = newName.match(regex);
                const fraseEncontrada = match[0].trim();
                const restoDelNombre = newName.replace(regex, '').trim();
                newName = `${fraseEncontrada} ${restoDelNombre}`;
                break;
            }
        }

        newName = newName.replace(/\s([a-zA-Z])$/,
                                  (match, letter) => ` ${letter.toUpperCase()}`);
        return newName;
    }



    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);

                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;
                        }
                    }
                }

                // --- INICIO: NUEVA LÓGICA PARA 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()) : "";


                    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;
                        }
                    }

                    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;
                        }
                    }
                }
                // --- FIN: NUEVA LÓGICA PARA EVITAR DUPLICACIÓN ---

                return delimitadorPrevio + toValue;
            });
        }
        return newText;
    }

    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;
    }

    //**************************************************************************************************************************************
    // Nombre: renderPlacesInFloatingPanel
    // Autor: mincho77
    // Fecha: 2025-05-27
    // Descripción: Procesa y muestra los lugares con posibles inconsistencias en un panel flotante.
    //              Incluye la lógica para obtener detalles de cada lugar, aplicar normalizaciones,
    //              comparar con el nombre original y generar sugerencias.
    // Parámetros:
    //   places (Array): Un array de objetos 'venue' de WME para analizar.
    //**************************************************************************************************************************************
    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
        }

        //  console.log("[DEBUG] Preparando panel flotante. Total places a analizar:",places.length);

        // --- Funciones auxiliares para tipo y categoría ---
        function getPlaceCategoryName(venueFromOldModel, venueSDKObject)
        { // Acepta ambos tipos de venue
            let categoryId = null;
            let categoryName = null;
            // let source = ""; // Para saber de dónde vino la info (comentario de depuración eliminado)

            // 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";
        }


        //****************************************************************************************************
        // Nombre: getPlaceTypeInfo
        // Autor: mincho77
        // Fecha: 2025-05-27
        // Descripción: Determina si un lugar es un área o un punto y devuelve información iconográfica relacionada.
        // Parámetros:
        //   venue (Object): El objeto 'venue' de WME.
        // Retorna:
        //   Object: Un objeto con las propiedades 'isArea' (boolean), 'icon' (String) y 'title' (String).
        //****************************************************************************************************
        function getPlaceTypeInfo(venue)
        {
            const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null;
            const isArea = geometry?.CLASS_NAME?.endsWith("Polygon");
            return {
                isArea,
                icon : isArea ? "⭔" : "⊙", // Icono para área o punto
                title : isArea ? "Área" : "Punto"
            };
        }
        //****************************************************************************************************
//****************************************************************************************************
// Nombre: shouldForceSuggestionForReview
// Autor: Gemini AI / mincho77
// Fecha: 2025-05-27 // Ajusta la fecha según sea necesario
// Descripción: Determina si una palabra del diccionario debe mostrarse siempre como sugerencia
//              para revisión manual debido a tildes y letras/combinaciones específicas.
// Parámetros:
//   word (String): La palabra del diccionario a evaluar.
// Retorna:
//   Boolean: true si la palabra cumple los criterios, false en caso contrario.
//****************************************************************************************************
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);

    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', 'b', 'v','z','ll', 'y'
        // Podrías añadir más si es necesario, ej. 'z', 'b', 'v' si son problemáticas en tu contexto
    ];

    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
}
//****************************************************************************************************



        // --- 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
        // createFloatingPanel(0); // 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)
                                                    : []);


        async function processNextPlace()
        {
            // 1. Leer estados de checkboxes y configuraciones iniciales
            const chkHideMyEditsChecked = document.getElementById("chk-hide-my-edits")?.checked ?? false;

            const useFullPipeline =
              true; // Como definimos, siempre ejecutar el flujo completo
            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;

            // 2. Condición de salida principal (todos los lugares procesados)
            if (index >= places.length)
            {
                //console.log("[DEBUG] Todos los lugares procesados. Finalizando render...");
                finalizeRender(inconsistents, places);
                return;
            }

            const venueFromOldModel =
              places[index]; // Usaremos este para datos base y fallback
            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;

            // Protección extra si el place en el índice es inválido
            if (!places[index] || typeof places[index] !== 'object') {
               // console.warn(`[DEBUG] Lugar inválido o tipo inesperado en el índice ${index}:`, places[index]);
                updateScanProgressBar(index, places.length);
                index++;
                setTimeout(() => processNextPlace(), 0);
                return;
            }

            // 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(`[DEBUG] Lugar inválido o sin nombre en el índice ${index}:`, venueFromOldModel);
                updateScanProgressBar(index, places.length);
                index++;
                setTimeout(() => processNextPlace(), 0);
                return;
            }

            const originalName = venueFromOldModel?.attributes?.name?.value ||
                                 venueFromOldModel?.attributes?.name || '';
            const currentVenueId = venueFromOldModel.getID();

            // 4. --- OBTENER INFO DEL EDITOR Y DEFINIR wasEditedByMe (PRIORIZANDO
            // SDK) ---
            let lastEditorInfoForLog = "Editor: Desconocido";
            let lastEditorIdForComparison = null;
            let wasEditedByMe = false;

            let currentLoggedInUserId = null;
            let currentLoggedInUserName =
              null; // Para comparar si el SDK devuelve nombre

            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;
                }
            }

            if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues &&
                wmeSDK.DataModel.Venues.getById)
            {
                // console.log(`[SDK] Intentando obtener venue ID: ${currentVenueId} con sdk.DataModel.Venues.getById`);
                try
                {
                    const venueSDK = await wmeSDK.DataModel.Venues.getById(
                      { venueId : currentVenueId });

                    if (venueSDK && venueSDK.modificationData)
                    {
                        const updatedByDataFromSDK =
                          venueSDK.modificationData.updatedBy;
                        /*  console.log(
                            `[SDK] Para ID ${
                              currentVenueId}, venue.modificationData.updatedBy: `,
                            updatedByDataFromSDK,
                            `(Tipo: ${typeof updatedByDataFromSDK})`);*/

                        if (typeof updatedByDataFromSDK === 'string' &&
                            updatedByDataFromSDK.trim() !== '')
                        {
                            lastEditorInfoForLog =
                              `Editor (SDK): ${updatedByDataFromSDK}`;
                            resolvedEditorName =
                              updatedByDataFromSDK; // SDK provided username
                                                    // directly
                            if (currentLoggedInUserName &&
                                currentLoggedInUserName === updatedByDataFromSDK)
                            {
                                wasEditedByMe = true;
                            }
                        }
                        else if (typeof updatedByDataFromSDK === 'number')
                        {
                            lastEditorInfoForLog =
                              `Editor (SDK): ID ${updatedByDataFromSDK}`;
                            resolvedEditorName =
                              `ID ${updatedByDataFromSDK}`; // Default to ID
                            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; // Username found via W.model
                                }
                                else if (userObjectW)
                                {
                                    lastEditorInfoForLog = `Editor (SDK ID ${
                                      updatedByDataFromSDK} -> W.model): ID ${
                                      updatedByDataFromSDK} (sin userName en W.model)`;
                                    // resolvedEditorName remains `ID
                                    // ${updatedByDataFromSDK}`
                                }
                            }
                        }
                        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
                    { // venueSDK o venueSDK.modificationData no encontrados
                        lastEditorInfoForLog =
                          "Editor (SDK): No venue/modificationData. Usando fallback W.model.";
                        if (venueSDK)
                            console.log(
                              "[SDK] venueSDK.modificationData no encontrado. Venue SDK:",
                              venueSDK);
                        else
                            console.log("[SDK] venueSDK no fue obtenido para ID:", currentVenueId);

                        const oldModelUpdatedBy =
                          venueFromOldModel.attributes.updatedBy;
                        if (oldModelUpdatedBy !== null &&
                            oldModelUpdatedBy !== undefined)
                        {
                            lastEditorIdForComparison = oldModelUpdatedBy;
                            resolvedEditorName =
                              `ID ${oldModelUpdatedBy}`; // Default to ID
                            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; // Username found
                                }
                                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";
                        }
                    }
                }
                catch (e)
                {
                  //  console.error(`[SDK] Error al obtener venue ${currentVenueId} con el SDK:`, e);
                    lastEditorInfoForLog = "Editor: Error con SDK (usando fallback W.model)";
                    resolvedEditorName = "Error SDK"; // Initial state for error
                    const oldModelUpdatedBy =
                      venueFromOldModel.attributes.updatedBy;
                    if (oldModelUpdatedBy !== null &&
                        oldModelUpdatedBy !== undefined)
                    {
                        lastEditorIdForComparison = oldModelUpdatedBy;
                        resolvedEditorName =
                          `ID ${oldModelUpdatedBy}`; // Default to ID on fallback
                        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; // Username found
                            }
                            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";
                    }
                }
            }
            else
            {
                // Fallback completo a W.model si el SDK no está inicializado
                const oldModelUpdatedBy = venueFromOldModel.attributes.updatedBy;
                if (oldModelUpdatedBy !== null && oldModelUpdatedBy !== undefined)
                {
                    lastEditorIdForComparison = oldModelUpdatedBy;
                    resolvedEditorName = `ID ${oldModelUpdatedBy}`; // Default to ID
                    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; // Username found
                        }
                        else if (userObjectW)
                        {
                            usernameFromOldModel =
                              `ID ${oldModelUpdatedBy} (sin userName)`;
                        }
                    }
                    lastEditorInfoForLog =
                      `Editor (W.model): ${usernameFromOldModel}`;
                }
                else
                {
                    lastEditorInfoForLog = "Editor (W.model): N/D";
                    resolvedEditorName = "N/D";
                }
            }

            // Actualizar wasEditedByMe basado en ID numérico si lo tenemos
            if (currentLoggedInUserId !== null &&
                typeof lastEditorIdForComparison === 'number' &&
                currentLoggedInUserId === lastEditorIdForComparison)
            {
                wasEditedByMe = true;
            }

            //console.log(`[DEBUG] Usuario logueado: ${currentLoggedInUserName} (ID: ${currentLoggedInUserId})`);

            const editedByMeText = wasEditedByMe ? ' (Editado por mí)' : '';
            /*  console.log(`[INFO EDITOR LUGAR] Nombre: "${originalName}" (ID: ${
                currentVenueId}) | ${lastEditorInfoForLog}${editedByMeText}`);*/
            // ---- FIN INFO DEL EDITOR ----

            // 5. --- PROCESAMIENTO DEL NOMBRE PALABRA POR PALABRA ---
            let nombreSugeridoParcial = [];
            let sugerenciasLugar = {};
            const originalWords = originalName.split(/\s+/);
            const processingStepLabel = document.getElementById("processingStep");

            if (index === 0)
            {
                sugerenciasPorPalabra = {};
            }

            // Definir expresiones regulares fuera del bucle para eficiencia
            const romanRegexStrict = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/; // Sensible a mayúsculas
            const romanRegexStrictInsensitive = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i; // Insensible

            originalWords.forEach((P, idx) => {
                const endsWithComma = restoreCommas && P.endsWith(",");
                const baseWord = endsWithComma ? P.slice(0, -1) : P;
                const cleaned = baseWord.trim();

                if (cleaned === "") {
                    nombreSugeridoParcial.push(cleaned);
                    return;
                }

                let isExcluded = false;
                let matchingExcludedWord = null;
                if (checkExcludedWords) {
                    matchingExcludedWord = excludedArray.find(
                      w_excluded => removeDiacritics(w_excluded.toLowerCase()) ===
                                    removeDiacritics(cleaned.toLowerCase()));
                    isExcluded = !!matchingExcludedWord;
                }

                let tempReplaced;
                const isCommon = commonWords.includes(cleaned.toLowerCase());
                // Usar la versión insensible para la detección inicial flexible de si ES un romano
                const isPotentiallyRomanNumeral = romanRegexStrictInsensitive.test(cleaned);

                if (isExcluded) {
                    tempReplaced = matchingExcludedWord;
                     // Si la palabra excluida es un número romano, asegurarse de que esté en mayúsculas
                    if (romanRegexStrictInsensitive.test(tempReplaced)) {
                        tempReplaced = tempReplaced.toUpperCase();
                    }
                } else {
                    let dictionaryFormToUse = null;

                    // Paso 1: Consultar el diccionario
                    if (checkDictionaryWords && window.dictionaryWords && typeof window.dictionaryWords.forEach === "function") {
                        const cleanedLowerNoDiacritics = removeDiacritics(cleaned.toLowerCase());
                        for (const diccWord of window.dictionaryWords) {
                            if (removeDiacritics(diccWord.toLowerCase()) === cleanedLowerNoDiacritics) {
                                // Si la palabra original es potencialmente romana, solo aceptamos del diccionario si también está en MAYÚSCULAS.
                                if (isPotentiallyRomanNumeral) {
                                    if (diccWord === diccWord.toUpperCase() && romanRegexStrict.test(diccWord)) { // Verificar que la forma del dicc también sea un romano válido en mayus
                                        dictionaryFormToUse = diccWord;
                                    }
                                } else {
                                    dictionaryFormToUse = diccWord;
                                }
                                break;
                            }
                        }
                    }

                    // Paso 2: Decidir la forma base
                    if (dictionaryFormToUse !== null) {
                        tempReplaced = dictionaryFormToUse;
                    } else {
                        // normalizePlaceName ya maneja la capitalización de romanos a MAYUS si no viene del diccionario
                        tempReplaced = normalizePlaceName(cleaned);
                    }

                    // Paso 3: Ajustes finales de capitalización y forzar MAYUS para romanos
                    if (isPotentiallyRomanNumeral) {
                        // Si se detectó como romano (incluso si el diccionario dio algo mixto o normalizePlaceName falló),
                        // forzar a mayúsculas. Usamos la expresión regular estricta (sensible a mayúsculas)
                        // para validar que la forma final sea un romano correcto.
                        const upperVersion = tempReplaced.toUpperCase();
                        if (romanRegexStrict.test(upperVersion)) { // Validar que la versión en mayúsculas sea un romano
                           tempReplaced = upperVersion;
                        } else {
                           // Si al pasar a mayúsculas deja de ser un romano válido (raro, pero posible si cleaned era "mixToRoman"),
                           // entonces aplicar capitalización normal.
                           tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1).toLowerCase();
                        }
                    } else if (idx === 0) { // Primera palabra y no es romano
                        tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
                    } else { // No es la primera palabra y no es romano
                        if (isCommon) {
                            tempReplaced = tempReplaced.toLowerCase();
                        } else {
                            // No es común, no es primera, no es romano: Capitalizar.
                            tempReplaced = tempReplaced.charAt(0).toUpperCase() + tempReplaced.slice(1);
                        }
                    }
                }

                // <<< DEBUG LOG INICIAL PARA "Panaderia" / "Cafe" >>>
                if (cleaned.toLowerCase() === "panaderia" || cleaned.toLowerCase() === "cafe" || cleaned.toLowerCase() === "ii") {
                    console.log(`[DEBUG WORD FINAL] Original: "${cleaned}", isExcluded: ${isExcluded}, matchingExcluded: "${matchingExcludedWord}", tempReplaced: "${tempReplaced}"`);
                }

                // Ahora, la lógica para generar las SUGERENCIAS CLICKEABLES del diccionario.
                if (!isExcluded && checkDictionaryWords && window.dictionaryWords &&
                    typeof window.dictionaryWords.forEach === "function") {

                    const processedWordForCompare = removeDiacritics(tempReplaced.toLowerCase());

                    window.dictionaryWords.forEach(diccWord => {
                        const diccWordForCompare = removeDiacritics(diccWord.toLowerCase());

                        if (processedWordForCompare === diccWordForCompare) {
                            if (diccWord !== tempReplaced) {
                                if (!sugerenciasLugar[baseWord]) {
                                    sugerenciasLugar[baseWord] = [];
                                }
                                if (!sugerenciasLugar[baseWord].some(s => s.word === diccWord && s.fuente === 'dictionary')) {
                                    sugerenciasLugar[baseWord].push({
                                        word: diccWord,
                                        similarity: 1,
                                        fuente: 'dictionary'
                                    });
                                }
                            }
                        }
                    });
                }

                // Sugerencias de palabras excluidas (se mantiene igual)
                if (checkExcludedWords) {
                    const similarExcluded =
                      findSimilarWords(cleaned, excludedArray, similarityThreshold)
                        .filter(s => s.similarity < 1);
                    if (similarExcluded.length > 0) {
                        sugerenciasLugar[baseWord] =
                          (sugerenciasLugar[baseWord] || [])
                            .concat(similarExcluded.map(
                              s => ({...s, fuente: 'excluded' })));
                    }
                }

                if (endsWithComma && !tempReplaced.endsWith(",")) {
                    tempReplaced += ",";
                }
                nombreSugeridoParcial.push(tempReplaced);
            });
            // ---- FIN PROCESAMIENTO PALABRA POR PALABRA ----

            // 6. --- COMPILACIÓN DE suggestedName ---
            const joinedSuggested = nombreSugeridoParcial.join(' ');

            let processedName = joinedSuggested;
            if (applyGeneralReplacements) {
                processedName = aplicarReemplazosGenerales(processedName);
            }
            processedName = aplicarReglasEspecialesNombre(processedName);
            processedName = postProcessQuotesAndParentheses(processedName);

            if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0) {
                processedName = aplicarReemplazosDefinidos(processedName, replacementWords);
            }

            let suggestedName = processedName.replace(/\s{2,}/g, ' ').trim(); // Nombre base sugerido

            // ***** NUEVO: Eliminar punto solo si está al FINAL del nombre completo sugerido *****
            if (suggestedName.endsWith('.')) {
                suggestedName = suggestedName.slice(0, -1);
            }
            // ***** FIN NUEVO *****

            // ---- FIN COMPILACIÓN DE suggestedName ----
            console.log(`[WME PLN DEBUG] Original: "${originalName}", Procesado palabra a palabra (joined): "${joinedSuggested}", Sugerido Final (antes de skip): "${suggestedName}"`);

            // 7. --- LÓGICA DE SALTO (SKIP) CONSOLIDADA ---
            const tieneSugerencias = Object.keys(sugerenciasLugar).length > 0;
            let shouldSkipThisPlace = false;
            let skipReasonLog = "";

            if (originalName.trim() === suggestedName.trim())
            {
                shouldSkipThisPlace = true;
                skipReasonLog = `[SKIP EXACT MATCH] Descartado "${
                  originalName}" porque es idéntico al nombre sugerido "${
                  suggestedName}".`;
            }
            // --- INICIO: LÓGICA DE SALTO POR "OMITIR MIS EDICIONES" ---
            else if (chkHideMyEditsChecked && wasEditedByMe)
            {
                shouldSkipThisPlace = true;
                skipReasonLog = `[SKIP MY EDIT] Descartado "${
                  originalName}" (ID: ${
                  currentVenueId}) porque fue editado por mí y la opción "Omitir mis ediciones" está activa.`;
            }
            // --- FIN: LÓGICA DE SALTO POR "OMITIR MIS EDICIONES" ---
            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();

                if (tempOriginalNormalized.toLowerCase() === suggestedName.toLowerCase() && !tieneSugerencias)
                {
                    shouldSkipThisPlace = true;
                    skipReasonLog = `[SKIP NORMALIZED] Se descartó "${
                      originalName}" (normalizado a "${tempOriginalNormalized}") porque ya coincide con el sugerido "${
                      suggestedName}" y no hay otras sugerencias.`;
                }
            }
            // ---- FIN LÓGICA DE SALTO ---

            // 8. Registrar o no en la lista de inconsistentes
            if (shouldSkipThisPlace)
            {
                if (skipReasonLog)
                    console.log(skipReasonLog);
            }
            else
            {
                if (processingStepLabel)
                {
                    processingStepLabel.textContent =
                      "Registrando lugar(es) con inconsistencias...";
                }

                let categoryNameToStore = "Sin categoría (Error)";
                if (venueFromOldModel)
                {
                    let venueSDKObjectForCategory =
                      null; // Necesitaríamos obtener el venueSDKObject de nuevo
                            // aquí si no lo guardamos o pasarlo si lo guardamos
                            // antes. Para esta iteración, si el SDK dio el venue
                            // antes, podríamos reusarlo, pero por simplicidad,
                            // getPlaceCategoryName puede intentar obtenerlo si le
                            // pasamos el ID, o solo usar W.model por ahora.
                            // simplificar y solo pasar venueFromOldModel,
                            // getPlaceCategoryName usará W.model.
                            // use el SDK para categoría, getPlaceCategoryName debe
                            // modificarse para aceptar ID y hacer la llamada async
                            // al SDK.
                    try
                    {
                        // Pasamos el venue del W.model; getPlaceCategoryName
                        // intentará usar SDK si wmeSDK está disponible y se le pasa
                        // el venueSDK Para esto, necesitaríamos haber guardado el
                        // venueSDK que obtuvimos arriba. Por ahora,
                        // getPlaceCategoryName ya intenta usar un
                        // venueSDKObject si se le pasa. Lo obtendremos de nuevo
                        // aquí para la categoría para mantenerlo encapsulado en
                        // esta prueba.
                        let venueSDKForCategory = null;
                        if (wmeSDK && wmeSDK.DataModel && wmeSDK.DataModel.Venues &&
                            wmeSDK.DataModel.Venues.getById)
                        {
                            try
                            {
                                venueSDKForCategory =
                                  await wmeSDK.DataModel.Venues.getById(
                                    { venueId : currentVenueId });
                            }
                            catch (catSDKError)
                            {
                                console.error("[SDK CATEGORY] Error obteniendo venueSDK para categoría:", catSDKError);
                            }
                        }
                        categoryNameToStore = getPlaceCategoryName(
                          venueFromOldModel, venueSDKForCategory);
                    }
                    catch (e)
                    {
                        console.error("Error llamando a getPlaceCategoryName desde processNextPlace:", e);
                    }
                }

                inconsistents.push({
                    id : currentVenueId,
                    original : originalName, // Mantener el original real
                    normalized : suggestedName, // El sugerido ya procesado y sin punto final
                    category : categoryNameToStore,
                    editor : resolvedEditorName
                });
                sugerenciasPorPalabra[currentVenueId] = sugerenciasLugar;
            }

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

        /*   console.log("[DEBUG] ¿excludedArray es array?:",
                       Array.isArray(excludedArray));
           console.log("[DEBUG] ¿processNextPlace está definido?:",
                       typeof processNextPlace === "function");
           console.log("[DEBUG] Justo antes de llamar a processNextPlace");*/
        // Inicializar procesamiento incremental
        try
        {
            // console.log("[DEBUG] Justo antes de llamar a processNextPlace");
            setTimeout(() => { processNextPlace(); }, 10);
        }
        catch (error)
        {
            console.error("[ERROR] Fallo en processNextPlace:", error);
        }
        // Fallback automático si finalizeRender no se ejecuta
        scanFallbackTimeoutId = setTimeout(() => {
            if (!document.querySelector("#wme-place-inspector-output")?.innerHTML)
            {
               /* console.warn(
                  "[WME PLN] Fallback activado: finalizeRender no se llamó en 30s. Mostrando panel flotante vacío.");
                console.error(
                  "[WME PLN] Error: finalizeRender no se completó. Se invocó fallback tras 30 segundos de espera.");*/
                createFloatingPanel("results", 0); // <--- CAMBIO AQUÍ: Llamar con "results" y 0 inconsistents si hay fallback
                const outputFallback =
                  document.querySelector("#wme-place-inspector-output");
                if (outputFallback) // Re-seleccionar por si acaso
                {
                    outputFallback.innerHTML =
                      "El proceso tomó demasiado tiempo o se interrumpió. No se completó el análisis.";
                }
            }
        }, 30000);


        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(' ');
        }


        // Mostrar el panel flotante solo al terminar el procesamiento
        // (mover esta llamada al final del procesamiento)

        // --- Función para finalizar renderizado una vez completado el análisis ---
        function finalizeRender(inconsistents, placesArr)
        {
            // alert("🟢 Entrando a finalizeRender"); // ← punto 6
            //  Log de depuración al entrar a finalizeRender

            /*  console.log("[WME PLN] Finalizando render. Inconsistentes
               encontrados:", inconsistents.length);*/
            // 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";
            }
            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); // <--- CAMBIO AQUÍ: Llamar con "results"

            if (output) // Ya no necesitamos output.innerHTML = ""; porque createFloatingPanel lo hace
            {
                // 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 totalInconsistents = inconsistents.length;
            if (totalInconsistents > maxRenderLimit)
            {
                inconsistents = inconsistents.slice(0, maxRenderLimit);

                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);

                    const message = document.createElement("p");
                    message.innerHTML = `Se encontraron <strong>${
                      totalInconsistents}</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);

                    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
                }
            }
            // Si no hay inconsistencias, mostrar mensaje y salir (progreso visible)
            if (inconsistents.length === 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);
                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;
            }

            // Mostrar spinner solo si hay inconsistencias a procesar
            // showLoadingSpinner();

            const table = document.createElement("table");
            table.style.width = "100%";
            table.style.borderCollapse = "collapse";
            table.style.fontSize = "12px";

            const thead = document.createElement("thead");
            const headerRow = document.createElement("tr");
            [
                "Permalink",
                "Tipo",
                "Categoría",
                "Editor",
                "Nombre Actual",
                "Nombre Sugerido",
                "Sugerencias de reemplazo",
                "Acción"
            ].forEach(header => {
                const th = document.createElement("th");
                th.textContent = header;
                th.style.borderBottom = "1px solid #ccc";
                th.style.padding = "4px";
                th.style.textAlign = "left";
                headerRow.appendChild(th);
            });

            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);

            const tbody = document.createElement("tbody");

            inconsistents.forEach(({ id, original, normalized, category, editor },
                                   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.width = "65px";
                row.appendChild(permalinkCell);

                // Columna Tipo de place
                const venue = W.model.venues.getObjectById(id);
                const { icon : typeIcon, title : typeTitle } =
                  getPlaceTypeInfo(venue);
                const typeCell = document.createElement("td");
                typeCell.textContent = typeIcon;
                typeCell.title = `Lugar tipo ${typeTitle}`;
                typeCell.style.padding = "4px";
                typeCell.style.width = "65px";
                row.appendChild(typeCell);

                // Columna Categoría del place
                const categoryCell = document.createElement("td");
                const categoryName = getPlaceCategoryName(venue);
                categoryCell.textContent = categoryName;
                categoryCell.title = `Categoría: ${categoryName}`;
                categoryCell.style.padding = "4px";
                categoryCell.style.width = "130px";
                row.appendChild(categoryCell);


                // 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");
                // Nueva columna: sugerencia de reemplazo seleccionada
                const suggestionListCell = document.createElement("td");
                suggestionListCell.style.padding = "4px";
                suggestionListCell.style.fontSize = "11px";
                suggestionListCell.style.color = "#333";
                // Permitir múltiples líneas en la celda de sugerencias
                suggestionListCell.style.whiteSpace = "pre-wrap";
                suggestionListCell.style.wordBreak = "break-word";
                suggestionListCell.style.width = "270px";
                // Calcular sugerencias similares de especiales aquí
                // --- Nueva lógica para separar sugerencias por fuente ---
                const allSuggestions = sugerenciasPorPalabra?.[id] || {};
                const similarList = {};
                const similarDictList = {};
                Object.entries(allSuggestions)
                  .forEach(([ originalWord, suggestions ]) => {
                      suggestions.forEach(s => {
                          if (s.fuente === 'excluded')
                          {
                              if (!similarList[originalWord])
                                  similarList[originalWord] = [];
                              similarList[originalWord].push(s);
                          }
                          else if (s.fuente === 'dictionary')
                          {
                              if (!similarDictList[originalWord])
                                  similarDictList[originalWord] = [];
                              similarDictList[originalWord].push(s);
                          }
                      });
                  });

                // --- NUEVA LÓGICA: aplicar reemplazos automáticos y solo mostrar
                // sugerencias < 1 ---
                let autoApplied = false;
                let localNormalized = normalized;
                // 1. Procesar sugerencias de especiales (similarList)
                let hasExcludedSuggestionsToShow = false;
                if (similarList && Object.keys(similarList).length > 0)
                {
                    Object.entries(similarList)
                      .forEach(([ originalWord, suggestions ]) => {
                          suggestions.forEach(s => {
                              if (s.similarity === 1)
                              {
                                  // Si encontramos una sugerencia 100%, ya fue
                                  // aplicada en el pipeline principal.
                                  autoApplied = true;
                                  // NO mostrar sugerencia clickable ni texto en
                                  // columna de sugerencias.
                              }
                              else if (s.similarity < 1)
                              {
                                  hasExcludedSuggestionsToShow = true;
                              }
                          });
                      });
                }
                // 2. Procesar sugerencias de diccionario (similarDictList)
                let hasDictSuggestionsToShow = false;
                if (similarDictList && Object.keys(similarDictList).length > 0)
                {
                    Object.entries(similarDictList)
                      .forEach(([ originalWord, suggestions ]) => {
                          suggestions.forEach(s => {
                              if (s.similarity < 1)
                              {
                                  hasDictSuggestionsToShow = true;
                              }
                          });
                      });
                }

                // --- EVITAR DUPLICADOS DE SUGERENCIAS ENTRE "ESPECIALES" Y
                // "DICCIONARIO" --- Crear set de palabras ya procesadas en
                // sugerencias especiales
                const palabrasYaProcesadas = new Set();
                if (similarList && Object.keys(similarList).length > 0)
                {
                    Object.keys(similarList)
                      .forEach(palabra =>
                                 palabrasYaProcesadas.add(palabra.toLowerCase()));
                }

                // Render input de sugerencia
                const inputReplacement = document.createElement("input");
                inputReplacement.type = "text";
                let mainSuggestion = localNormalized;
                inputReplacement.value = mainSuggestion;
                inputReplacement.style.width = "270px";
                // Visual cue if change was due to excluded word
                if (localNormalized !== normalized)
                {
                    inputReplacement.style.backgroundColor =
                      "#fff3cd"; // color amarillo claro
                    inputReplacement.title = "Contiene palabra excluida reemplazada";
                }
                else if (autoApplied)
                {
                    inputReplacement.style.backgroundColor =
                      "#c8e6c9"; // verde claro
                    inputReplacement.title =
                      "Reemplazo automático aplicado (100% similitud)";
                }
                else if (normalized !== inputReplacement.value)
                {
                    inputReplacement.style.backgroundColor =
                      "#e6f7ff"; // Azul claro para cambios automáticos del
                                 // diccionario (≥ 90%)
                    inputReplacement.title =
                      "Cambio automático basado en diccionario (≥ 90%)";
                }
                else
                {
                    inputReplacement.title = "Nombre normalizado";
                }
                // NUEVA LÓGICA: marcar si contiene palabra sugerida por el
                // diccionario
                const palabrasDelDiccionario = new Set();
                Object.values(similarDictList).forEach(arr => {
                    arr.forEach(s => {
                        if (mainSuggestion.toLowerCase().includes(
                              s.word.toLowerCase()))
                        {
                            palabrasDelDiccionario.add(s.word.toLowerCase());
                        }
                    });
                });
                if (palabrasDelDiccionario.size > 0)
                {
                    inputReplacement.style.backgroundColor = "#cce5ff"; // Azul claro
                    inputReplacement.title =
                      "Contiene sugerencias del diccionario aplicadas manualmente";
                }
                suggestionCell.appendChild(inputReplacement);
                suggestionCell.style.padding = "4px";
                suggestionCell.style.width = "270px";

                // --- Función debounce ---
                function debounce(func, delay) {
                    let timeout;
                    return function (...args) {
                        clearTimeout(timeout);
                        timeout = setTimeout(() => func.apply(this, args), delay);
                    };
                }

                // --- Activar/desactivar el botón Aplicar según si hay cambios ---
                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 con debounce (puede personalizarse la lógica) ---
                inputOriginal.addEventListener('input', debounce(() => {
                    // Opcional: alguna lógica si se desea manejar cambios en inputOriginal
                }, 300));

                // Renderizar solo sugerencias < 1 en sugerencias de reemplazo
                // especiales primero
                if (similarList && Object.keys(similarList).length > 0) {
                    Object.entries(similarList).forEach(([originalWord, suggestions]) => {
                        suggestions.forEach(s => { // 's' aquí es { word: candidate, similarity: sim, fuente: 'excluded' }
                            // Mostrar todas las sugerencias < 100% de similitud,
                            // o incluso las de 100% si la forma en la lista de excluidas es diferente (ej. mayúsculas/minúsculas)
                            // a la originalWord.
                            // La condición original s.similarity < 1 es buena para evitar sugerir lo mismo que ya está.
                            if (s.similarity < 1 || (s.similarity === 1 && originalWord !== s.word) ) {
                                const suggestionDiv = document.createElement("div");
                                const icono = "🏷️"; // Icono para palabras especiales/excluidas
                                suggestionDiv.textContent =
                                  `${icono} ¿"${originalWord}" por "${s.word}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`;
                                suggestionDiv.style.cursor = "pointer";
                                suggestionDiv.style.padding = "2px 4px";
                                suggestionDiv.style.margin = "2px 0";
                                suggestionDiv.style.border = "1px solid #ddd";
                                suggestionDiv.style.backgroundColor = "#f3f9ff"; // Color distintivo si quieres

                                suggestionDiv.addEventListener("click", () => {
                                    const currentSuggestedValue = inputReplacement.value; // Valor actual del campo "Nombre Sugerido"

                                   // console.log("--- DEBUG: Clic en Sugerencia ESPECIAL ---");
                                   // console.log("Palabra Original (base de la sugerencia):", originalWord);
                                   // console.log("Sugerencia (palabra a usar de lista Especiales):", s.word);

                                    // 1. Normalizamos la 'originalWord' para saber qué forma buscar
                                    const normalizedOriginalWord = normalizePlaceName(originalWord);
                                    //console.log("Forma Normalizada de Palabra Original (para buscar):", normalizedOriginalWord);

                                    // 2. Normalizamos la palabra sugerida 's.word' de la lista de Especiales
                                    const wordToInsert = normalizePlaceName(s.word);
                                    //console.log("Palabra Normalizada para Insertar:", wordToInsert);

                                    //console.log("Valor ACTUAL del campo 'Nombre Sugerido':", currentSuggestedValue);

                                    // 3. Creamos la regex para buscar la 'normalizedOriginalWord'
                                    const searchRegex = new RegExp("\\b" + escapeRegExp(normalizedOriginalWord) + "\\b", "gi");
                                    //console.log("Regex para buscar en 'Nombre Sugerido':", searchRegex.toString());

                                    if (!searchRegex.test(currentSuggestedValue)) {
                                        console.warn("¡ADVERTENCIA ESPECIAL! La forma normalizada de la palabra original ('" + normalizedOriginalWord + "') no se encontró en el 'Nombre Sugerido' actual ('" + currentSuggestedValue + "'). No se hará reemplazo.");
                                    }

                                    const newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert);
                                    //console.log("Valor DESPUÉS de .replace() (Especial):", newSuggestedValue);

                                    if (currentSuggestedValue !== newSuggestedValue)
                                    {
                                        inputReplacement.value = newSuggestedValue;
                                       // console.log("Campo 'Nombre Sugerido' ACTUALIZADO (Especial) a:", newSuggestedValue);
                                    }
                                    else
                                    {
                                        console.log("No hubo cambios en 'Nombre Sugerido' (Especial) (el nuevo valor es idéntico al anterior o la palabra a reemplazar no se encontró/ya era igual).");
                                    }
                                    inputReplacement.dispatchEvent(new Event("input"));
                                   // console.log("--- FIN DEBUG (Especial) ---");
                                });
                                suggestionListCell.appendChild(suggestionDiv);
                            }
                        });
                    });
                }
                // Diccionario después, evitando duplicados de palabras ya sugeridas
                // en especiales
                if (similarDictList && Object.keys(similarDictList).length > 0)
                {
                    Object.entries(similarDictList)
                      .forEach(([ originalWord, suggestions ]) => { // originalWord es la palabra del nombre del lugar ANTES de normalizePlaceName (la que fue clave en sugerenciasLugar)
                          if (palabrasYaProcesadas.has(originalWord.toLowerCase()))
                              return;
                          suggestions.forEach(s => { // s es { word: diccWordDelDiccionario, similarity: sim, fuente: 'dictionary' }

                              const normalizedOriginalWordForDisplay = normalizePlaceName(originalWord);
                              const normalizedDictionaryWordToApply = normalizePlaceName(s.word);

                              // Condición A: Si la aplicación de la sugerencia del diccionario
                              // resulta en el nombre COMPLETO que ya está sugerido (localNormalized), Y la palabra original
                              // (normalizada) también era igual a ese nombre completo, entonces la palabra es única y ya está perfecta.
                              // Principalmente para nombres de una sola palabra.
                              if (normalizedDictionaryWordToApply === localNormalized && normalizedOriginalWordForDisplay === localNormalized) {
                                  return;
                              }

                              // Condición B: Si la palabra original (normalizada para mostrar) es idéntica
                              // a la palabra del diccionario (normalizada para aplicar), entonces la sugerencia
                              // sería del tipo "¿X por X?", lo cual es inútil y no ofrece ningún cambio para esa palabra específica.
                              if (normalizedOriginalWordForDisplay === normalizedDictionaryWordToApply) {
                                  return;
                              }

                              // Si hemos pasado ambas condiciones, significa que la sugerencia ofrece un cambio útil.
                              const suggestionItem = document.createElement("div");
                              const icono = "📘";

                              suggestionItem.textContent = `${icono} ¿"${normalizedOriginalWordForDisplay}" por "${normalizedDictionaryWordToApply}"? (simil. ${(s.similarity * 100).toFixed(0)}%)`;

                              suggestionItem.style.cursor = "pointer";
                              suggestionItem.style.padding = "2px 4px";
                              suggestionItem.style.margin = "2px 0";
                              suggestionItem.style.border = "1px solid #ddd";
                              suggestionItem.style.backgroundColor = "#f9f9f9";

                              suggestionItem.addEventListener("click", () => {
                                  const currentSuggestedValue = inputReplacement.value;

                                  const wordToInsert = normalizedDictionaryWordToApply;
                                  const wordToSearchAndReplace = normalizedOriginalWordForDisplay; // Usar la forma normalizada de la palabra original para la búsqueda

                                  const searchRegex = new RegExp("\\b" + escapeRegExp(wordToSearchAndReplace) + "\\b", "gi");

                                  let newSuggestedValue = currentSuggestedValue;
                                  if (searchRegex.test(currentSuggestedValue)) {
                                      newSuggestedValue = currentSuggestedValue.replace(searchRegex, wordToInsert);
                                  } else {
                                       console.warn(`¡ADVERTENCIA! La palabra a reemplazar ('${wordToSearchAndReplace}') no se encontró en el 'Nombre Sugerido' actual ('${currentSuggestedValue}').`);
                                  }

                                  if (inputReplacement.value !== newSuggestedValue) {
                                      inputReplacement.value = newSuggestedValue;
                                  } else {
                                       console.log("No hubo cambios efectivos en 'Nombre Sugerido' o la palabra a reemplazar no se encontró.");
                                  }
                                  inputReplacement.dispatchEvent(new Event("input"));
                              });
                              suggestionListCell.appendChild(suggestionItem);
                          });
                      });
                }
                row.appendChild(suggestionCell);
                row.appendChild(suggestionListCell);

                const actionCell = document.createElement("td");
                actionCell.style.padding = "4px";
                actionCell.style.width = "120px";

                const buttonGroup = document.createElement("div");
                buttonGroup.style.display = "flex";
                buttonGroup.style.gap = "4px";

                const applyButton = document.createElement("button");
                applyButton.textContent = "✔";
                applyButton.title = "Aplicar sugerencia";
                applyButton.style.padding = "4px 8px";
                applyButton.style.cursor = "pointer";

                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;

                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();
                       /* const commonWords = [
                            'de',  'del',   'el',   'la', 'los', 'las',
                            'y',   'e',     'o',    'u',  'un',  'una',
                            'y',   'unos',  'unas', 'a',  'en',  'con',
                            'sin', 'sobre', 'tras', 'por'
                        ];*/
                        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 => {
                            // Antes de añadir, podrías verificar si ya existe para evitar
                            // trabajo innecesario, aunque un Set maneja duplicados.
                            // También, considera si quieres aplicar isValidExcludedWord aquí,
                            // aunque usualmente las palabras del nombre del lugar son candidatas directas.
                            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, () => {
                    waitForWazeAPI(() => {
                        const places = getVisiblePlaces();
                    renderPlacesInFloatingPanel(places);
                    setTimeout(reactivateAllActionButtons,
                               250); // Esperar a que el panel se reconstruya
                    });
            });
            W.model.actionManager.events.register("afterredoaction", null, () => {
                    waitForWazeAPI(() => {
                        const places = getVisiblePlaces();
                        renderPlacesInFloatingPanel(places);
                    setTimeout(reactivateAllActionButtons, 250);
                });
            });
            // 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, excludedWords, threshold)
    {
        const userThreshold =
          parseFloat(document.getElementById("similarityThreshold")?.value ||
                     "85") /
          100;
        const lowerWord = word.toLowerCase();
        // excludedWords is now always an array
        const firstChar = lowerWord.charAt(0);
        let candidates = excludedWords;

        if (typeof excludedWords === 'object' && !Array.isArray(excludedWords))
        {
            // Estamos usando el índice
            candidates = excludedWords[firstChar] || [];
        }

        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.";
    }
    ///***************************************************************************
    // Nombre: createFloatingPanel
    // Autor: mincho77
    // Fecha: 2025-05-26 // Actualiza la fecha si es necesario
    // Descripción: Crea el panel flotante con dimensiones y títulos correctos, y ajusta la posición del panel de resultados.
    //***************************************************************************
    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"; // ESTA LÍNEA

            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);

            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);

            const outputDivLocal = document.createElement("div");
            outputDivLocal.id = "wme-place-inspector-output";
            outputDivLocal.style.fontSize = "14px";
            outputDivLocal.style.backgroundColor = "#fdfdfd";
            outputDivLocal.style.overflowY = "auto"; // Y ESTA
            floatingPanelElement.appendChild(outputDivLocal);

            document.body.appendChild(floatingPanelElement);
        }

        const titleElement = floatingPanelElement.querySelector("#wme-pln-panel-title");
        const outputDiv = floatingPanelElement.querySelector("#wme-place-inspector-output");

        if(outputDiv) outputDiv.innerHTML = "";

        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/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/wBjD4Zv2Iv+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/wBs/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/gXSLLRbSSQytBYW9dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/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 aquí 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);

                    const similarityLabel = document.createElement("label");
                    similarityLabel.textContent = "Similitud mínima para sugerencia de palabras:";
                    similarityLabel.title = "Ajusta el umbral mínimo de similitud (entre 80% y 95%) que se usará para sugerencias de reemplazo en nombres. El 100% es un reemplazo directo.";
                    similarityLabel.style.fontSize = "12px";
                    similarityLabel.style.display = "block";
                    similarityLabel.style.marginTop = "8px";
                    similarityLabel.style.marginBottom = "4px";
                    containerGeneral.appendChild(similarityLabel);

                    const similaritySlider = document.createElement("input");
                    similaritySlider.type = "range";
                    similaritySlider.min = "80";
                    similaritySlider.max = "95";
                    similaritySlider.value = "85";
                    similaritySlider.id = "similarityThreshold";
                    similaritySlider.style.width = "100%";
                    similaritySlider.title = "Desliza para ajustar la similitud mínima";
                    const similarityValueDisplay = document.createElement("span");
                    similarityValueDisplay.textContent = similaritySlider.value + "%";
                    similarityValueDisplay.style.marginLeft = "8px";
                    similaritySlider.addEventListener("input", () => {
                        similarityValueDisplay.textContent =
                        similaritySlider.value + "%";
                    });
                    containerGeneral.appendChild(similaritySlider);
                    containerGeneral.appendChild(similarityValueDisplay);

                    // ELIMINAR ESTO (SI SE APLICÓ EL CAMBIO ANTERIOR):
                    // const editorFilterWrapper = document.getElementById("editorFilterInput")?.closest("div");
                    // if (editorFilterWrapper) editorFilterWrapper.remove();

                    // --- 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";
                    // Opcional: Cargar preferencia desde localStorage
                    // const savedHideMyEdits = localStorage.getItem('WMEPlnHOM');
                    // if (savedHideMyEdits !== null) {
                    //    hideMyEditsCheckbox.checked = savedHideMyEdits === 'true';
                    // }
                    // hideMyEditsCheckbox.addEventListener('change', (event) => {
                    //    localStorage.setItem('WMEPlnHOM', event.target.checked);
                    // });


                    const hideMyEditsLabel = document.createElement("label");
                    // --- MODIFICACIÓN AQUÍ ---
                    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;
                    // --- FIN MODIFICACIÓN ---
                    hideMyEditsLabel.htmlFor = "chk-hide-my-edits";
                    hideMyEditsLabel.style.fontSize = "13px";
                    hideMyEditsLabel.style.cursor = "pointer";

                    hideMyEditsWrapper.appendChild(hideMyEditsCheckbox);
                    hideMyEditsWrapper.appendChild(hideMyEditsLabel);
                   // containerGeneral.appendChild(hideMyEditsWrapper);
                    // --- FIN: Checkbox para omitir mis ediciones ---

                    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



    function waitForSidebarAPI()
    {
        if (W && W.userscripts && W.userscripts.registerSidebarTab)
        {
            const savedExcluded = localStorage.getItem("excludedWordsList");
            if (savedExcluded)
            {
                try
                {
                    const parsed = JSON.parse(savedExcluded);
                    excludedWords = new Set(parsed);
                    /*   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);
                    window.dictionaryIndex = {};
                    parsed.forEach(word => {
                        const letter = word.charAt(0).toLowerCase();
                        if (!window.dictionaryIndex[letter])
                        {
                            window.dictionaryIndex[letter] = [];
                        }
                        window.dictionaryIndex[letter].push(word);
                    });
                    /*  console.log(
                        "[WME PLN] Diccionario restaurado desde localStorage:",
                        parsed);*/
                }
                catch (e)
                {
                    /* console.error(
                       "[WME PLN] Error al cargar dictionaryWordsList del
                       localStorage:", e);*/
                    window.dictionaryWords = new Set();
                }
            }
            else
            {
                window.dictionaryWords = new Set();
                //  console.log("[WME PLN] No se encontró diccionario en
                //  localStorage.");
            }
            loadReplacementWordsFromStorage();
            waitForWazeAPI(() => { createSidebarTab(); });
        }
        else
        {
            // console.log("[WME PLN] Esperando W.userscripts API...");
            setTimeout(waitForSidebarAPI, 1000);
        }
    }


    // 1. MODIFICAR normalizePlaceName
    function normalizePlaceName(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("/");
        }

        // Casos especiales como "MI" y "DI"
        if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI") {
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        // Números romanos: todo en mayúsculas. No elimina puntos aquí.
        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)) {
            return word.toUpperCase();
        }

        // Si hay un número seguido de letra (sin espacio), convertir la letra en mayúscula
        word = word.replace(/(\d)([a-zÁÉÍÓÚÑáéíóúñ])/gi, (_, num, letter) => `${num}${letter.toUpperCase()}`);

        let normalizedWord;
        if (/^[0-9]+$/.test(word)) { // Solo números
            normalizedWord = word;
        } else if (/^[A-ZÁÉÍÓÚÑ0-9.]+$/.test(word) && word.length > 1 && word.includes('.'))
        {
            // Si es todo mayúsculas (o números) Y CONTIENE UN PUNTO (ej. "St."), mantener como está.
            // Podrías añadir más heurísticas aquí si es necesario para acrónimos con puntos.
            normalizedWord = word;
        }
        else
        { // Capitalización estándar
            normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        // NO eliminamos el punto aquí. Eso se hará al final del nombre completo del place si es necesario.
        // Ejemplo: "St." se mantendrá como "St." después de esta función.

        return normalizedWord;
    }

    //***************************************************************************
    // Nombre: normalizeWordInternal
    // Autor: mincho77
    // Fecha: 2025-05-27
    // Descripción: Usada por postProcessQuotesAndParentheses. Similar a normalizePlaceName
    //              pero con contexto de si es la primera palabra o dentro de comillas/paréntesis.
    //***************************************************************************
    function normalizeWordInternal(word, isFirstWordInSequence = false, isInsideQuotesOrParentheses = false) {
        if (!word || typeof word !== "string") return "";

        // Replicar lógica esencial de normalizePlaceName para consistencia
        if (word.toUpperCase() === "MI" || word.toUpperCase() === "DI") {
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        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)) {
            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('.')) {
            resultWord = word; // Mantener "St." dentro de comillas/paréntesis
        }
         else if (isInsideQuotesOrParentheses && /^[A-ZÁÉÍÓÚÑ0-9]+$/.test(word) && word.length > 1) {
            resultWord = word; // Mantener acrónimos sin puntos
        }
         else {
            resultWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        }

        // NO eliminamos el punto aquí tampoco.
        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(); // Inicializada en waitForSidebarAPI
        let replacementWords = {};     // { "Urb.": "Urbanización", ... }
        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);
                input.value = "";
                renderExcludedWordsList(
                  document.getElementById("excludedWordsList"));
                saveExcludedWordsToLocalStorage(); // Asumiendo que tienes esta
                                                   // función
            });
            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 (ignorando mayúsculas).");
                    return;
                }

                window.dictionaryWords.add(newWord);
                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);
    }

    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.");
    }

    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);
        }
    }



    // 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);
        });
    }


    function exportSharedDataToXml()
    {
        if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
        {
            alert(
              "No hay palabras especiales ni reemplazos definidos para exportar.");
            return;
        }

        let xmlParts = [];
        // Exportar palabras excluidas
        Array.from(excludedWords)
          .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
          .forEach(w => xmlParts.push(`    <word>${xmlEscape(w)}</word>`));

        // Exportar reemplazos
        Object.entries(replacementWords)
          .sort((a, b) => a[0].toLowerCase().localeCompare(
                  b[0].toLowerCase())) // Ordenar por 'from'
          .forEach(([ from, to ]) => {
              xmlParts.push(`    <replacement from="${xmlEscape(from)}">${
                xmlEscape(to)}</replacement>`);
          });

        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;
        // Puedes darle un nombre genérico ya que contiene ambos tipos de datos
        a.download = "wme_normalizer_data_export.xml";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

        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))
                            { // Asegúrate que isValidExcludedWord no lo bloquee si
                            // viene de XML
                                const validation =
                                isValidExcludedWord(val); // Revalidar o ajustar esta
                                                            // lógica para importación
                                if (validation.valid)
                                { // Omitir esta validación si el XML es la fuente de
                                // verdad
                                    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)
                            { // Permitir que 'to' esté vacío si se quiere "reemplazar
                            // con nada"
                                if (replacementWords.hasOwnProperty(from) &&
                                    replacementWords[from] !== to)
                                {
                                    replacementsOverwritten++;
                                }
                                else if (!replacementWords.hasOwnProperty(from))
                                {
                                    newReplacementsAdded++;
                                }
                                replacementWords[from] = to;
                            }
                        }

                        // Guardar y Re-renderizar AMBAS listas
                        saveExcludedWordsToLocalStorage(); // Asumo que tienes esta
                                                        // función o la adaptas
                        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.");
            }
        }



        function createReplacementsManager(parentContainer)
        {
            parentContainer.innerHTML = ''; // Limpiar por si acaso

            const title = document.createElement("h4");
            title.textContent = "Gestión de Reemplazos de Palabras/Frases";
            title.style.fontSize = "15px";
            title.style.marginBottom = "10px";
            parentContainer.appendChild(title);

            // 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

            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";
            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";

            fromInputContainer.appendChild(fromLabel);
            fromInputContainer.appendChild(fromInput);
            addSection.appendChild(fromInputContainer);


            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";
            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);

            fromInput.setAttribute('spellcheck', 'false');
            toInput.setAttribute('spellcheck', 'false');


            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);
            parentContainer.appendChild(addSection);

            // 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";
            parentContainer.appendChild(listElement);

            // 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;
            }
            if (fromValue === toValue)
            {
                alert("El texto original y el de reemplazo no pueden ser iguales.");
                return;
            }
            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();
            });




       // Coloca esta función junto a aplicarReemplazosGenerales y aplicarReglasEspecialesNombre
        /*function aplicarReemplazosDefinidos(text, replacementRules)
        {
            let newText = text;
            if (typeof replacementRules !== 'object' || 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 casos como
            // "D1 Super" y "D1" donde "D1 Super" debe procesarse primero.
            const sortedFromKeys = Object.keys(replacementRules).sort((a, b) => b.length - a.length);

            for (const fromKey of sortedFromKeys) {
                const toValue = replacementRules[fromKey];

                // Crear una expresión regular para encontrar 'fromKey' como palabra completa,
                // insensible a mayúsculas/minúsculas (flag 'gi').
                // escapeRegExp es importante si 'fromKey' puede contener caracteres especiales de regex.
                const regex = new RegExp('\\b' + escapeRegExp(fromKey) + '\\b', 'giu'); // <--- MODIFICADO AQUÍ: Añadido flag 'u'

                newText = newText.replace(regex, (matchedInText, offset, originalString) => {
                    // matchedInText: La cadena que coincidió en el texto (ej. "d1", "D1").
                    // fromKey: La clave canónica del objeto replacementWords (ej. "D1").
                    // toValue: El valor de reemplazo canónico (ej. "Tiendas D1").

                    // Condición para evitar reemplazo redundante:
                    // Verificar si 'matchedInText' ya forma parte de 'toValue' en el contexto correcto.
                    // Ej: fromKey="D1", toValue="Tiendas D1".
                    // Si encontramos "D1" en "Mis Tiendas D1", queremos evitar cambiarlo a "Mis Tiendas Tiendas D1".

                    // Encontrar dónde estaría 'fromKey' (canónico) dentro de 'toValue' (canónico)
                    // para alinear la comprobación. Hacemos esta parte insensible a mayúsculas para la búsqueda de índice.
                    const fromKeyLower = fromKey.toLowerCase();
                    const toValueLower = toValue.toLowerCase();
                    const indexOfFromInTo = toValueLower.indexOf(fromKeyLower);

                    if (indexOfFromInTo !== -1) {
                        // Calculamos el inicio de donde podría estar el 'toValue' completo en el 'originalString'
                        const potentialExistingToStart = offset - indexOfFromInTo;

                        if (potentialExistingToStart >= 0 &&
                            (potentialExistingToStart + toValue.length) <= originalString.length) {

                            const substringInOriginal = originalString.substring(potentialExistingToStart, potentialExistingToStart + toValue.length);

                            // Comparamos (insensible a mayúsculas) si el 'toValue' ya existe en esa posición
                            if (substringInOriginal.toLowerCase() === toValueLower) {
                                // Ya existe el 'toValue' completo en el lugar correcto, así que no reemplazamos.
                                // Devolvemos el texto que coincidió originalmente.
                                return matchedInText;
                            }
                        }
                    }
                    // Si no se cumple la condición anterior, realizamos el reemplazo con 'toValue'.
                    return toValue;
                });
            }
            return newText;
        }*/


        // 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";

        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);

        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);
        parentContainer.appendChild(actionButtonsContainer);

        const dropArea = document.createElement("div");
        dropArea.textContent =
          "Arrastra aquí el archivo XML (contiene Excluidas y Reemplazos)";
        // ... (estilos para dropArea como en createExcludedWordsManager) ...
        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]); // Usar una función manejadora compartida
        });
        parentContainer.appendChild(dropArea);

        // Cargar y renderizar la lista inicial
        renderReplacementsList(listElement);
    }

    // === Renderizar lista de palabras excluidas ===
    function renderExcludedWordsList(ulElement, filter = "")
    { // AHORA RECIBE ulElement
        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;
            }
        }

        const currentFilter = filter.toLowerCase();
        /*   console.log("[WME PLN] Renderizando lista. Filtro:",
                       currentFilter,
                       "Total palabras:",
                       excludedWords.size);*/
        ulElement.innerHTML = ""; // Limpiar lista anterior

        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";

                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);

                const iconContainer = document.createElement("span");
                iconContainer.style.display = "flex";
                iconContainer.style.gap = "8px"; // Más espacio entre iconos

                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
                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);
                        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);
                        renderExcludedWordsList(ulElement, currentFilter);
                    }
                });

                iconContainer.appendChild(editBtn);
                iconContainer.appendChild(deleteBtn);
                li.appendChild(iconContainer);
                ulElement.appendChild(li);
            });
        }

        try
        {
            localStorage.setItem("excludedWordsList",
                                 JSON.stringify(Array.from(excludedWords)));
            // console.log("[WME PLN] Lista guardada en localStorage:",
            // 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.
        }
    }

    // Nueva función: renderDictionaryList
    function renderDictionaryList(ulElement, filter = "")
    {
        if (!ulElement || !window.dictionaryWords)
            return;
        const currentFilter = filter.toLowerCase();
        ulElement.innerHTML = "";

        const wordsToRender =
          Array.from(window.dictionaryWords)
            .filter(word => word.toLowerCase().startsWith(currentFilter))
            .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

        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;
        }

        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";

            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);

            const iconContainer = document.createElement("span");
            iconContainer.style.display = "flex";
            iconContainer.style.gap = "8px";

            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);
                }
            });

            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}' 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);
        }
    }

    function exportExcludedWordsList()
    {
        if (excludedWords.size === 0 && Object.keys(replacementWords).length === 0)
        {
            alert("No hay palabras especiales ni reemplazos para exportar.");
            return;
        }
        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");
        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>";
        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_excluded_words_export.xml";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    function exportDictionaryWordsList()
    {
        if (window.dictionaryWords.size === 0)
        {
            alert(
              "La lista de palabras del diccionario está vacía. Nada que exportar.");
            return;
        }
        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>`;
        const blob =
          new Blob([ xmlContent ],
                   { type : "application/xml;charset=utf-8" }); // Añadir charset
        const url = URL.createObjectURL(blob);
        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);
    }

    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;
            }
        });
    }

    waitForSidebarAPI();
    })();

    // 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);
    }

QingJ © 2025

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