WME Places Name Normalizer

Normaliza nombres de lugares en Waze Map Editor (WME)

目前为 2025-05-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         WME Places Name Normalizer
// @namespace    https://gf.qytechs.cn/en/users/mincho77
// @version      6.1.1
// @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();

    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; // Declarar wmeSDK global

    function tryInitializeSDK(finalCallback)
    {
        let attempts = 0;
        const maxAttempts = 60; // Aumentado a 30 segundos (60 * 500ms)
        const intervalTime = 500;
        let sdkAttemptInterval = null; // Guardar la referencia al intervalo
       // console.log("[SDK INIT ATTEMPT] Iniciando intentos para obtener getWmeSdk().");
        function attempt()
        {
           // console.log(`[SDK INIT ATTEMPT] Intento #${attempts + 1}`);
            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(); // Llama al callback final
                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(); // Llama al callback final igualmente
            }
        }
        // Iniciar el intervalo para los reintentos.
        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, 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",
        "Hotel",
        "Restaurante"
    ];

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

        // Crear una expresión regular para encontrar 'fromKey' como palabra completa (usando \b),
        // de forma global ('g') e insensible a mayúsculas/minúsculas ('i').
        // escapeRegExp es crucial si 'fromKey' puede contener caracteres especiales de regex.
        const regex = new RegExp('\\b' + escapeRegExp(fromKey) + '\\b', 'gi');

        newText = newText.replace(regex, (matchedInText, offset, originalString) => {
            // Parámetros del callback de replace:
            // matchedInText: La cadena exacta que coincidió en el texto (ej. "d1", "D1", etc.).
            // offset: La posición inicial de 'matchedInText' en 'originalString'.
            // originalString: La cadena completa sobre la que se está haciendo el reemplazo en esta iteración.

            // Convertimos a minúsculas para la lógica de comparación y búsqueda de índice.
            const fromKeyLower = fromKey.toLowerCase(); // La clave canónica de tus reglas, en minúsculas.
            const toValueLower = toValue.toLowerCase();   // El valor de reemplazo canónico, en minúsculas.

            // Encontrar dónde estaría 'fromKeyLower' dentro de 'toValueLower'.
            const indexOfFromInTo = toValueLower.indexOf(fromKeyLower);

            if (indexOfFromInTo !== -1) {
                // Si 'fromKey' es parte de 'toValue' (ej. from="D1", to="Tiendas D1").
                // Calculamos el inicio de donde el 'toValue' completo podría ya existir en 'originalString',
                // alineándolo con la posición actual de 'matchedInText'.
                const potentialExistingToStart = offset - indexOfFromInTo;

                // Verificar que el substring potencial esté dentro de los límites de originalString.
                if (potentialExistingToStart >= 0 &&
                    (potentialExistingToStart + toValue.length) <= originalString.length) {

                    // Extraer el substring del texto original que podría ser el 'toValue' ya existente.
                    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) {
                        // ¡Coincidencia! El 'toValue' completo ya está presente y contextualiza
                        // correctamente al 'matchedInText'. No reemplazamos para evitar duplicados.
                        // Devolvemos el texto que coincidió originalmente sin cambios.
                        return matchedInText;
                    }
                }
            }
            // Si no se cumplió la condición de "ya existe contextualizado", realizamos el reemplazo.
            // Se usa 'toValue' (con su capitalización original definida en tus reglas).
            return 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;
}

function renderPlacesInFloatingPanel(places)
{
    createFloatingPanel(0); // Asegurar que el panel flotante exista antes de renderizar
    const maxPlacesToScan = parseInt(document.getElementById("maxPlacesInput")?.value || "100", 10);
    if (places.length > maxPlacesToScan)
    {
        places = places.slice(0, maxPlacesToScan);
    }

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

        // Intento 1: Usar el venueSDKObject si está disponible y tiene la info
        if (venueSDKObject)
        {
            // Suposición 1: El SDK tiene una propiedad 'mainCategory' que es un
            // objeto con 'id' y 'name'
            if (venueSDKObject.mainCategory && venueSDKObject.mainCategory.id)
            {
                categoryId = venueSDKObject.mainCategory.id;
                if (venueSDKObject.mainCategory.name)
                {
                    categoryName = venueSDKObject.mainCategory.name;
                    source = "SDK (mainCategory.name)";
                }
            }
            // Suposición 2: El SDK tiene un array 'categories', y el primer
            // elemento tiene 'id' y '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)";
                    }
                }
                // Suposición 3: El primer elemento del array 'categories' es
                // directamente el nombre
                else if (typeof firstCategorySDK === 'string')
                {
                    categoryName = firstCategorySDK;
                    source = "SDK (categories[0] as string)";
                    // No tendríamos ID en este caso desde esta fuente directa.
                }
            }
            // Suposición 4: El SDK tiene solo el ID de la categoría principal
            // en una propiedad simple
            else if (venueSDKObject.primaryCategoryID)
            { // Nombre de propiedad hipotético
                categoryId = venueSDKObject.primaryCategoryID;
                source = "SDK (primaryCategoryID)";
            }
            // (Añade más suposiciones aquí basándote en lo que veas en
            // console.dir(venueSDKObject))
        }

        // Si tenemos un nombre de categoría del SDK, lo usamos.
        if (categoryName)
        {
            //  console.log(`[CATEGORÍA] Usando nombre de categoría de  ${source}: ${categoryName} ${categoryId ? `(ID: ${categoryId})` : ''}`);
            return categoryName;
        }

        // Intento 2: Si no obtuvimos nombre del SDK pero sí un ID, o si no
        // usamos el SDK, usamos W.model Primero, obtener el categoryId si aún
        // no lo tenemos (del venueFromOldModel)
        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";
        }

        // Ahora, resolver el categoryId (sea del SDK o de W.model) usando
        // W.model.categories
        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}`);
            return categoryObjWModel.attributes.name;
        }

        // Fallback final: si solo tenemos el ID y no pudimos resolver el nombre
        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.`);
            return `${categoryId}`;
        }

        return "Sin categoría";
    }


    function getPlaceTypeInfo(venue)
    {
        const geometry = venue?.getOLGeometry ? venue.getOLGeometry() : null;
        const isArea = geometry?.CLASS_NAME?.endsWith("Polygon");
        return {
            isArea,
            icon : isArea ? "⭔" : "⊙",
            title : isArea ? "Área" : "Punto"
        };
    }

    // --- 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)"; // Altura para el panel flotante

    // 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 hideMyEdits = false;
        const useFullPipeline =
          true; // Como definimos, siempre ejecutar el flujo completo
        const applyGeneralReplacements =
          useFullPipeline ||
          (document.getElementById("chk-general-replacements")?.checked ??
           true); // Asume que este ID podría existir si reañades el check
        const checkExcludedWords =
          useFullPipeline ||
          (document.getElementById("chk-check-excluded")?.checked ?? false);
        const checkDictionaryWords =
          true; // O leer de checkbox si lo reintroduces
        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 = {};
        }

        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 = cleaned;
            const isArticle = commonWords.includes(cleaned.toLowerCase());

            if (isExcluded)
            {
                tempReplaced = matchingExcludedWord;
            }
            else
            {
                if (isArticle)
                {
                    tempReplaced = cleaned.toLowerCase();
                    if (idx === 0 && tempReplaced.length > 0)
                    {
                        tempReplaced = tempReplaced.charAt(0).toUpperCase() +
                                       tempReplaced.slice(1);
                    }
                }
                else
                {
                    tempReplaced = normalizePlaceName(cleaned);
                }
            }

            if (!isExcluded && checkDictionaryWords && window.dictionaryWords &&
                typeof window.dictionaryWords.forEach === "function")
            {
                window.dictionaryWords.forEach(diccWord => {
                    const cleanTemp =
                      removeDiacritics(tempReplaced.toLowerCase());
                    const cleanDicc = removeDiacritics(diccWord.toLowerCase());
                    if (cleanTemp.charAt(0) !== cleanDicc.charAt(0))
                        return;
                    const sim = calculateSimilarity(cleanTemp, cleanDicc);
                    if (sim >= similarityThreshold)
                    { // Usar el umbral del slider
                        if (!sugerenciasLugar[baseWord])
                            sugerenciasLugar[baseWord] = [];
                        if (!sugerenciasLugar[baseWord].some(
                              s => s.word.toLowerCase() ===
                                   diccWord.toLowerCase()))
                        {
                            sugerenciasLugar[baseWord].push({
                                word : diccWord,
                                similarity : sim,
                                fuente : 'dictionary'
                            });
                        }
                    }
                });
            }
            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(' ');
        // const cleanedSuggestedForLog = // Esta variable parece no usarse para la lógica, solo para logs.
        //   joinedSuggested.replace(/\s{2,}/g, ' ').trim();

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


        // ---- APLICAR REEMPLAZOS DEFINIDOS POR EL USUARIO ----
        if (typeof replacementWords === 'object' && Object.keys(replacementWords).length > 0) {
            // console.log(`[REEMPLAZOS] Antes: "${processedName}"`, JSON.stringify(replacementWords)); // Para depuración
            processedName = aplicarReemplazosDefinidos(processedName, replacementWords); // La llamada
            // console.log(`[REEMPLAZOS] Después: "${processedName}"`); // Para depuración
        }
        // ---- FIN NUEVO PASO ----




        const suggestedName = processedName.replace(/\s{2,}/g, ' ').trim();
        // ---- FIN COMPILACIÓN DE 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}".`;
        }
        else
        {
            const normalizedCleanCheck = suggestedName.toLowerCase();
            const originalCleanCheck =
              originalName.replace(/\s+/g, ' ').trim().toLowerCase();
            const isAlreadyNormalizedCheck =
              normalizedCleanCheck === originalCleanCheck;

            if (isAlreadyNormalizedCheck && !tieneSugerencias)
            {
                shouldSkipThisPlace = true;
                skipReasonLog = `[SKIP] Se descartó "${
                  originalName}" porque ya se considera normalizado (${
                  originalCleanCheck} vs ${
                  normalizedCleanCheck}) y sin sugerencias.`;
            }
            else if (hideMyEdits && wasEditedByMe)
            {
                shouldSkipThisPlace = true;
                skipReasonLog = `[SKIP MY EDIT] Descartado "${
                  originalName}" (ID: ${
                  currentVenueId}) porque fue editado por mí y la opción está activa.`;
            }
        }
        // ---- 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 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,
                normalized : suggestedName,
                category : categoryNameToStore,
                editor : resolvedEditorName // Add the resolved editor name here
            });
            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(0); // Mostrar panel aunque esté vacío
            const output =
              document.querySelector("#wme-place-inspector-output");
            if (output)
            {
                output.innerHTML =
                  "El proceso tomó demasiado tiempo o se interrumpió. No se completó el análisis.";
            }
        }
    }, 30000);
    // 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;
        }
        if (output)
        {
            output.innerHTML =
              ""; // Limpiar el mensaje de procesamiento y spinner
            // Mostrar el panel flotante al terminar el procesamiento
            createFloatingPanel(inconsistents.length);
        }
        // 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 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 = "20px";
                modal.style.zIndex = "10000";
                modal.style.width = "400px";
                modal.style.boxShadow = "0 0 15px rgba(0,0,0,0.3)";
                modal.style.borderRadius = "8px";
                modal.style.fontFamily = "sans-serif";

                // Fondo suave azul y mejor presentación
                modal.style.backgroundColor = "#f0f8ff";
                modal.style.border = "1px solid #aad";
                modal.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.2)";

                // --- Insertar ícono visual de información arriba del mensaje
                // ---
                const icon = document.createElement("div");
                icon.innerHTML = "ℹ️";
                icon.style.fontSize = "24px";
                icon.style.marginBottom = "10px";
                modal.appendChild(icon);

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

                modal.appendChild(acceptBtn);
                document.body.appendChild(modal);
            }
        }
        // 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";
        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 ]) => {
                      if (palabrasYaProcesadas.has(originalWord.toLowerCase()))
                          return;
                      suggestions.forEach(s => {
                          if (s.similarity < 1 ||
                              (s.similarity === 1 && originalWord !== s.word))
                          {
                              const suggestionItem =
                                document.createElement("div");
                              const icono = s.fuente === 'dictionary' ? "📘"
                                            : s.fuente === 'excluded' ? "🏷️"
                                                                      : "❓";
                              suggestionItem.textContent = `${icono} ¿"${
                                originalWord}" por "${s.word}"? (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; // Valor actual del campo "Nombre Sugerido"

                        // originalWord: Es la palabra del nombre original del lugar que generó esta sugerencia (ej. "ROJA")
                        // s.word: Es la palabra del diccionario/lista de especiales (ej. "rojo")

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

                        // 1. Normalizamos la 'originalWord' para saber qué forma buscar en el 'Nombre Sugerido' actual.
                        //    Esto asume que el 'Nombre Sugerido' ya contiene formas normalizadas.
                        const normalizedOriginalWord = normalizePlaceName(originalWord);
                        //console.log("Forma Normalizada de Palabra Original (para buscar):", normalizedOriginalWord);

                        // 2. Normalizamos la palabra sugerida 's.word' para la inserción.
                        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' en el 'currentSuggestedValue'.
                        //    Usamos 'gi' para que sea global (reemplace todas las ocurrencias) e insensible a mayúsculas.
                        //    Esto es importante porque 'normalizePlaceName' podría haber cambiado la capitalización.
                        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! La forma normalizada de la palabra original ('" + normalizedOriginalWord + "') no se encontró en el 'Nombre Sugerido' actual ('" + currentSuggestedValue + "'). No se hará reemplazo.");
                            // Consideración: Si 'originalWord' era, por ejemplo, una abreviatura que ya fue expandida
                            // por la pestaña de "Reemplazos Automáticos", entonces 'normalizedOriginalWord'
                            // (que sería la normalización de la abreviatura) tampoco se encontraría.
                            // En este caso, no hacer nada es probablemente correcto.
                        }

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

                        if (currentSuggestedValue !== newSuggestedValue)
                        {
                            inputReplacement.value = newSuggestedValue;
                            // console.log("Campo 'Nombre Sugerido' ACTUALIZADO a:", newSuggestedValue);
                        }
                        else
                        {
                            console.log("No hubo cambios en 'Nombre Sugerido' (el nuevo valor es idéntico al anterior o la palabra a reemplazar no se encontró/ya era igual).");
                        }
                        inputReplacement.dispatchEvent(new Event("input")); // Para actualizar el estado del botón Aplicar (✔)
                        //console.log("--- FIN DEBUG ---");
                    });
                              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);
                }
            });
            deleteButton.addEventListener("click", () => {
                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 #ccc";
                confirmModal.style.padding = "20px";
                confirmModal.style.zIndex = "10001";
                confirmModal.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)";
                confirmModal.style.fontFamily = "sans-serif";
                confirmModal.style.borderRadius = "6px";

                const message = document.createElement("p");
                const venue = W.model.venues.getObjectById(id);
                const placeName =
                  venue?.attributes?.name?.value || "este lugar";
                message.textContent =
                  `¿Estás seguro que deseas eliminar "${placeName}" del mapa?`;
                confirmModal.appendChild(message);

                const buttonWrapper = document.createElement("div");
                buttonWrapper.style.display = "flex";
                buttonWrapper.style.justifyContent = "flex-end";
                buttonWrapper.style.gap = "10px";
                buttonWrapper.style.marginTop = "10px";

                const cancelBtn = document.createElement("button");
                cancelBtn.textContent = "Cancelar";
                cancelBtn.style.padding = "4px 8px";
                cancelBtn.addEventListener("click",
                                           () => { confirmModal.remove(); });

                confirmBtn.addEventListener("click", () => {
                    const checked = modal.querySelectorAll("input[type=checkbox]:checked");
                    let anyAdded = false;
                    checked.forEach(c => {
                        const validation = isValidExcludedWord(c.value);
                        if (!validation.valid)
                        {
                            alert(`"${c.value}": ${validation.msg}`);
                            return;
                        }
                        excludedWords.add(c.value);
                        anyAdded = true;
                    });
                    if (anyAdded)
                        renderExcludedWordsList();
                    // Actualizar estado de botón aplicar según diferencia
                    if (suggestionInput.value !== original)
                    {
                        applyButton.disabled = false;
                        applyButton.style.color = "";
                    }
                    else
                    {
                        applyButton.disabled = true;
                        applyButton.style.color = "#bbb";
                    }
                    modal.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 = "10000";
                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.";
}

function createFloatingPanel(numInconsistents = 0)
{
    const existing = document.querySelector("#wme-place-inspector-panel");
    if (existing)
    {

        existing.style.display = 'block';
        return;
    }

    const panel = document.createElement("div");
    const closeBtn = document.createElement("span");
    closeBtn.textContent = "×";
    closeBtn.style.position = "absolute";
    closeBtn.style.top = "5px";
    closeBtn.style.right = "8px";
    closeBtn.style.cursor = "pointer";
    closeBtn.style.fontSize = "16px";
    closeBtn.style.color = "#aaa";
    closeBtn.title = "Cerrar panel";
    closeBtn.addEventListener("click", () => {
        panel.style.display = 'none'; // Ocultar el panel
        // Restaurar estado del botón escanear
        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 iconCheck = scanBtn.querySelector("span");
            if (iconCheck)
            {
                iconCheck.remove(); // Remover icono de check si quedó
            }
        }
        // Restaurar el mensaje en el tab
        const outputTab =
          document.getElementById("wme-normalization-tab-output");
        if (outputTab)
        {
            outputTab.textContent =
              "Presiona 'Start Scan...' para analizar los Lugares visibles.";
            outputTab.style.color = "#000";
            outputTab.style.fontWeight = "normal";
        }
        // Resetear barra de progreso/tab
        resetInspectorState();
    });
    panel.appendChild(closeBtn);
    panel.id = "wme-place-inspector-panel";
    panel.style.position = "fixed";
    panel.style.top = "55%";
    panel.style.left = "70%";
    panel.style.transform = "translate(-70%, -55%)";
    panel.style.width = "1500px";
    panel.style.height = "60vh";
    panel.style.overflow =
      "hidden"; // El scroll interno lo maneja #wme-place-inspector-output
    panel.style.zIndex = "9999";
    panel.style.background = "#fff";
    panel.style.border = "1px solid #ccc";
    panel.style.borderRadius = "6px";
    panel.style.boxShadow = "0 2px 6px rgba(0,0,0,0.2)";
    panel.style.padding = "10px";
    panel.style.fontFamily = "sans-serif";
    panel.style.display = 'block'; // Asegurar que esté visible al crear

    const title = document.createElement("h4");
    title.textContent =
      numInconsistents > 0
        ? `Lugares con Nombres No Normalizados (${numInconsistents})`
        : "Resultado del Análisis";
    title.style.marginTop = "0";
    title.style.marginBottom = "10px"; // Espacio antes de la tabla
    panel.appendChild(title);

    const output = document.createElement("div");
    output.id = "wme-place-inspector-output"; // Este es el output para la tabla
                                              // de lugares
    output.style.height = "calc(100% - 40px)";
    output.style.overflowY = "auto";
    output.style.border = "1px solid #eee";
    output.style.padding = "6px";
    output.style.fontSize = "12px";
    output.style.backgroundColor = "#f9f9f9";

    panel.appendChild(output);
    document.body.appendChild(panel);

    if (!document.getElementById("wme-pln-spinner-style"))
    {
        const styleTag = document.createElement("style");
        styleTag.id = "wme-pln-spinner-style";
        styleTag.textContent = `
                        @keyframes spin {
                          0% { transform: rotate(0deg); }
                          100% { transform: rotate(360deg); }
                        }
                    `;
        document.head.appendChild(styleTag);
    }
}

// Escuchar el botón Guardar de WME para resetear el inspector
const wmeSaveBtn = document.querySelector(
  "button.action.save, button[title='Guardar'], button[aria-label='Guardar']");
if (wmeSaveBtn)
{
    wmeSaveBtn.addEventListener("click", () => resetInspectorState());
}
function createSidebarTab()
{
    try
    {
        // 1. Verificar si WME y la función para registrar pestañas están listos
        if (!W || !W.userscripts ||
            typeof W.userscripts.registerSidebarTab !== 'function')
        {
            console.error("[WME PLN] WME (userscripts o registerSidebarTab) no está listo para crear la pestaña lateral.");
            return;
        }

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

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

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

        // 3. Inicializar las pestañas internas (General, Especiales,
        // Diccionario, Reemplazos)
        const tabsContainer = document.createElement("div");
        tabsContainer.style.display = "flex";
        tabsContainer.style.marginBottom = "8px";
        tabsContainer.style.gap = "8px";

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



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



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

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

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

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

function normalizePlaceName(word)
{
    if (!word || typeof word !== "string")
    {
        return "";
    }

    // --- Mejorar: Normalizar palabras dentro de paréntesis, quitando espacios
    // extra --- Detecta si la palabra está entre paréntesis, permitiendo
    // espacios
    const parenMatch = word.match(/^\(\s*(.+?)\s*\)$/);
    if (parenMatch)
    {
        let innerContent = parenMatch[1].replace(/\s+/g, ' ').trim(); // Contenido limpio dentro del paréntesis

        // Dividir el contenido interno en palabras y normalizar cada una
        const wordsInsideParentheses = innerContent.split(' ');
        const normalizedWordsInside = wordsInsideParentheses.map(singleWord => {
            // Aplicar la normalización básica de capitalización a cada palabra
            // (primera letra mayúscula, resto minúsculas),
            // ignorando aquí si `singleWord` está en `excludedWords` para este caso específico.
            if (!singleWord) return ""; // Manejar múltiples espacios que resultan en palabras vacías

            // Lógica de normalización de capitalización simple (puedes refinarla si es necesario)
            // Esta es la parte de normalizePlaceName que se encarga de la capitalización
            // cuando no es un número romano o "MI".
            if (singleWord.toUpperCase() === "MI")
            { // Mantener excepción para "MI"
                 return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
            }
            if (singleWord.toUpperCase() === "DI")
            { // Mantener excepción para "DI"
                 return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
            }
            const romanRegexStrict = /^M{0,3}(CM|CD?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
            if (romanRegexStrict.test(singleWord)) {
                return singleWord.toUpperCase();
            }
            // Normalización estándar de capitalización para otras palabras
            return singleWord.charAt(0).toUpperCase() + singleWord.slice(1).toLowerCase();
        });

        return "(" + normalizedWordsInside.join(' ') + ")";
    }

    // --- Lógica existente para palabras fuera de paréntesis (MANTENER) ---
    // Manejar palabras con "/" primero, normalizando cada parte recursivamente
    if (word.includes("/"))
    {
        return word.split("/").map(part => normalizePlaceName(part)).join("/");
    }

    // Excepción específica para "MI"
    if (word.toUpperCase() === "MI")
    {
        return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); // Resulta en "Mi"
    }

    // Regex para OTROS números romanos (incluyendo los que usan M, C, D, etc.)
    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))
    {
        // Si la palabra es un número romano válido, convertirla TODA a
        // mayúsculas.
        return word.toUpperCase();
    }
    else
    {
        // Si hay un número seguido de letra (sin espacio), convertir la letra
        // en mayúscula
        word = word.replace(/(\d)([a-z])/g, (_, num, letter) => `${num}${letter.toUpperCase()}`);
        // Si NO es un número romano, aplicar la normalización estándar:
        // primera letra mayúscula, resto minúsculas.
        return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
    }
}

// === 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', 'gi');

            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或关注我们的公众号极客氢云获取最新地址