WME Road Name Helper NP

Check suffix and common word abbreviations without leaving WME

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            WME Road Name Helper NP
// @description     Check suffix and common word abbreviations without leaving WME
// @version         2025.12.02.03
// @author          Kid4rm90s 
// @license         MIT
// @match           *://*.waze.com/*editor*
// @exclude         *://*.waze.com/user/editor*
// @connect         greasyfork.org
// @grant           GM_xmlhttpRequest
// @grant           GM_addStyle
// @namespace       https://greasyfork.org/users/1087400
// @require         https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js

// ==/UserScript==

(function () {
  'use strict';
  const updateMessage = `
Version 2025.12.02.03:
- <strong>FIXED</strong>: <br>Layer z-index conflict with WME Segment City Tool (changed from roads-2 to roads-3)
<br> - Highlight layer drifting above roads layer (added watchdog to maintain z-index) <br>
- <strong>CHANGED</strong>: <br> Highlight color from yellow to orange (#ff8800) for better visual distinction <br>
- Stroke width increased from 30 to 35 for improved visibility <br>
- <strong>IMPROVED</strong>: Layer persistence and stability with continuous z-index monitoring
`;
  const SCRIPT_VERSION = GM_info.script.version.toString();
  const SCRIPT_NAME = GM_info.script.name;
  const DOWNLOAD_URL = GM_info.script.downloadURL;
  const GreasyFork_URL = 'https://greasyfork.org/en/scripts/538171-wme-road-name-helper-np';
  const forumURL = 'https://greasyfork.org/en/scripts/538171-wme-road-name-helper-np/feedback';
  const SCRIPT_ID = 'wme-road-name-helper-np';
  const SCAN_DEBOUNCE_DELAY = 200; // 200ms delay after map movement stops
  const PROGRESS_UPDATE_THROTTLE = 10; // Update progress every N segments
  const RESCAN_DELAY_AFTER_FIX = 300; // Delay before rescanning after fix
  const MAX_SEGMENTS_TO_DISPLAY = 100; // Limit displayed segments for performance
  const LAYER_NAME = 'WME Road Name Helper NP'; // Layer name for highlighting
  
  let sdk;
  let currentMapExtent = null;
  let scanTimeout;
  let scannedSegments = [];
  let isScanning = false;
  let eventSubscriptions = [];
  let previewEnabled = false;
  
  // Cached UI elements
  const cachedElements = {
    scanCounter: null,
    fixAllButton: null,
    resultsContainer: null,
    progressBar: null,
    spinner: null
  };

  // Suffix Abbreviation Data (Abbreviation: FullWord)
  // This is for suffixes that have standard abbreviations
  const wmessa_approvedAbbr = {
    Ally: 'Alley',
    App: 'Approach',
    Arc: 'Arcade',
    Av: 'Avenue',
    Bwlk: 'Boardwalk',
    Bvd: 'Boulevard',
    Brk: 'Break',
    Bypa: 'Bypass',
    Ch: 'Chase',
    Cct: 'Circuit',
    Cl: 'Close',
    Con: 'Concourse',
    Ct: 'Court',
    Cr: 'Crescent',
    Crst: 'Crest',
    Dr: 'Drive',
    Ent: 'Entrance',
    Esp: 'Esplanade',
    Exp: 'Expressway',
    Ftrl: 'Firetrail',
    Fwy: 'Freeway',
    Glde: 'Glade',
    Gra: 'Grange',
    Gr: 'Grove',
    Hwy: 'Highway',
    Mwy: 'Motorway',
    Pde: 'Parade',
    Pwy: 'Parkway',
    Psge: 'Passage',
    Pl: 'Place',
    Plza: 'Plaza',
    Prom: 'Promenade',
    Qys: 'Quays',
    Rtt: 'Retreat',
    Rdge: 'Ridge',
    Rd: 'Road',
    Sq: 'Square',
    Stps: 'Steps',
    St: 'Street',
    Sbwy: 'Subway',
    Tce: 'Terrace',
    Trk: 'Track',
    Trl: 'Trail',
    Vsta: 'Vista',
  };

  // Suffix Suggestion Data (UserTyped/FullWord: CorrectAbbreviation)
  // This is for suffixes that have specific suggestions
  const wmessa_suggestedAbbr = {
    Alley: 'Ally',
    Approach: 'App',
    Arcade: 'Arc',
    Avenue: 'Av',
    Boardwalk: 'Bwlk',
    Boulevard: 'Bvd',
    Blvd: 'Bvd',
    Break: 'Brk',
    //Bypass: 'Bypa',
    Chase: 'Ch',
    Circuit: 'Cct',
    Close: 'Cl',
    Concourse: 'Con',
    Court: 'Ct',
    Crescent: 'Cr',
    Crest: 'Crst',
    Drive: 'Dr',
    Entrance: 'Ent',
    Esplanade: 'Esp',
    Expressway: 'Exp',
    Firetrail: 'Ftrl',
    Freeway: 'Fwy',
    Glade: 'Glde',
    Grange: 'Gra',
    Grove: 'Gr',
    Highway: 'Hwy',
    Ln: 'Lane',
    Marg: 'Marga',
    Motorway: 'Mwy',
    Parade: 'Pde',
    Parkway: 'Pwy',
    Passage: 'Psge',
    Place: 'Pl',
    Plaza: 'Plza',
    Promenade: 'Prom',
    Quays: 'Qys',
    Retreat: 'Rtt',
    Ridge: 'Rdge',
    Road: 'Rd',
    Square: 'Sq',
    Steps: 'Stps',
    Street: 'St',
    Subway: 'Sbwy',
    Terrace: 'Tce',
    Track: 'Trk',
    Trail: 'Trl',
    Vista: 'Vsta',
    रा१: 'रारा०१',
    रा२: 'रारा०२',
    रा३: 'रारा०३',
    रा४: 'रारा०४',
    रा५: 'रारा०५',
    रा६: 'रारा०६',
    रा७: 'रारा०७',
    रा८: 'रारा०८',
    रा९: 'रारा०९',
    रा१०: 'रारा१०',
    रा११: 'रारा११',
    रा१२: 'रारा१२',
    रा१३: 'रारा१३',
    रा१४: 'रारा१४',
    रा१५: 'रारा१५',
    रा१६: 'रारा१६',
    रा१७: 'रारा१७',
    रा१८: 'रारा१८',
    रा१९: 'रारा१९',
    रा२०: 'रारा२०',
    रा२१: 'रारा२१',
    रा२२: 'रारा२२',
    रा२३: 'रारा२३',
    रा२४: 'रारा२४',
    रा२५: 'रारा२५',
    रा२६: 'रारा२६',
    रा२७: 'रारा२७',
    रा२८: 'रारा२८',
    रा२९: 'रारा२९',
    रा३०: 'रारा३०',
    रा३१: 'रारा३१',
    रा३२: 'रारा३२',
    रा३३: 'रारा३३',
    रा३४: 'रारा३४',
    रा३५: 'रारा३५',
    रा३६: 'रारा३६',
    रा३७: 'रारा३७',
    रा३८: 'रारा३८',
    रा३९: 'रारा३९',
    रा४०: 'रारा४०',
    रा४१: 'रारा४१',
    रा४२: 'रारा४२',
    रा४३: 'रारा४३',
    रा४४: 'रारा४४',
    रा४५: 'रारा४५',
    रा४६: 'रारा४६',
    रा४७: 'रारा४७',
    रा४८: 'रारा४८',
    रा४९: 'रारा४९',
    रा५०: 'रारा५०',
    रा५१: 'रारा५१',
    रा५२: 'रारा५२',
    रा५३: 'रारा५३',
    रा५४: 'रारा५४',
    रा५५: 'रारा५५',
    रा५६: 'रारा५६',
    रा५७: 'रारा५७',
    रा५८: 'रारा५८',
    रा५९: 'रारा५९',
    रा६०: 'रारा६०',
    रा६१: 'रारा६१',
    रा६२: 'रारा६२',
    रा६३: 'रारा६३',
    रा६४: 'रारा६४',
    रा६५: 'रारा६५',
    रा६६: 'रारा६६',
    रा६७: 'रारा६७',
    रा६८: 'रारा६८',
    रा६९: 'रारा६९',
    रा७०: 'रारा७०',
    रा७१: 'रारा७१',
    रा७२: 'रारा७२',
    रा७३: 'रारा७३',
    रा७४: 'रारा७४',
    रा७५: 'रारा७५',
    रा७६: 'रारा७६',
    रा७७: 'रारा७७',
    रा७८: 'रारा७८',
    रा७९: 'रारा७९',
    रा८०: 'रारा८०',
    AH02: 'AH2',
  };

  // Suffixes that should be preserved in title case (case-insensitive)
  // These words will not be converted to lowercase in title case
  // This is useful for words that are proper nouns or have specific casing requirements.
  const wmessa_preserveCaseWords = [
    'NH01',
    'NH02',
    'NH03',
    'NH04',
    'NH05',
    'NH06',
    'NH07',
    'NH08',
    'NH09',
    'NH10',
    'NH11',
    'NH12',
    'NH13',
    'NH14',
    'NH15',
    'NH16',
    'NH17',
    'NH18',
    'NH19',
    'NH20',
    'NH21',
    'NH22',
    'NH23',
    'NH24',
    'NH25',
    'NH26',
    'NH27',
    'NH28',
    'NH29',
    'NH30',
    'NH31',
    'NH32',
    'NH33',
    'NH34',
    'NH35',
    'NH36',
    'NH37',
    'NH38',
    'NH39',
    'NH40',
    'NH41',
    'NH42',
    'NH43',
    'NH44',
    'NH45',
    'NH46',
    'NH47',
    'NH48',
    'NH49',
    'NH50',
    'NH51',
    'NH52',
    'NH53',
    'NH54',
    'NH55',
    'NH56',
    'NH57',
    'NH58',
    'NH59',
    'NH60',
    'NH61',
    'NH62',
    'NH63',
    'NH64',
    'NH65',
    'NH66',
    'NH67',
    'NH68',
    'NH69',
    'NH70',
    'NH71',
    'NH72',
    'NH73',
    'NH74',
    'NH75',
    'NH76',
    'NH77',
    'NH78',
    'NH79',
    'NH80',
    'AH1',
    'AH2',
    'AH3',
    'AH4',
    'AH5',
    'AH6',
    'AH7',
    'AH8',
    'AH9',
    'AH10',
    'AH42',
  ];

  // Highway Suffix Suggestion Data (EXACT match only)
  // This is for highway abbreviations that have specific suggestions
  const wmessa_suggestedHwyAbbr = {
    'NH01-': 'NH01 - रारा०१',
    'NH02-': 'NH02 - रारा०२',
    'NH03-': 'NH03 - रारा०३',
    'NH04-': 'NH04 - रारा०४',
    'NH05-': 'NH05 - रारा०५',
    'NH06-': 'NH06 - रारा०६',
    'NH07-': 'NH07 - रारा०७',
    'NH08-': 'NH08 - रारा०८',
    'NH09-': 'NH09 - रारा०९',
    'NH10-': 'NH10 - रारा१०',
    'NH11-': 'NH11 - रारा११',
    'NH12-': 'NH12 - रारा१२',
    'NH13-': 'NH13 - रारा१३',
    'NH14-': 'NH14 - रारा१४',
    'NH15-': 'NH15 - रारा१५',
    'NH16-': 'NH16 - रारा१६',
    'NH17-': 'NH17 - रारा१७',
    'NH18-': 'NH18 - रारा१८',
    'NH19-': 'NH19 - रारा१९',
    'NH20-': 'NH20 - रारा२०',
    'NH21-': 'NH21 - रारा२१',
    'NH22-': 'NH22 - रारा२२',
    'NH23-': 'NH23 - रारा२३',
    'NH24-': 'NH24 - रारा२४',
    'NH25-': 'NH25 - रारा२५',
    'NH26-': 'NH26 - रारा२६',
    'NH27-': 'NH27 - रारा२७',
    'NH28-': 'NH28 - रारा२८',
    'NH29-': 'NH29 - रारा२९',
    'NH30-': 'NH30 - रारा३०',
    'NH31-': 'NH31 - रारा३१',
    'NH32-': 'NH32 - रारा३२',
    'NH33-': 'NH33 - रारा३३',
    'NH34-': 'NH34 - रारा३४',
    'NH35-': 'NH35 - रारा३५',
    'NH36-': 'NH36 - रारा३६',
    'NH37-': 'NH37 - रारा३७',
    'NH38-': 'NH38 - रारा३८',
    'NH39-': 'NH39 - रारा३९',
    'NH40-': 'NH40 - रारा४०',
    'NH41-': 'NH41 - रारा४१',
    'NH42-': 'NH42 - रारा४२',
    'NH43-': 'NH43 - रारा४३',
    'NH44-': 'NH44 - रारा४४',
    'NH45-': 'NH45 - रारा४५',
    'NH46-': 'NH46 - रारा४६',
    'NH47-': 'NH47 - रारा४७',
    'NH48-': 'NH48 - रारा४८',
    'NH49-': 'NH49 - रारा४९',
    'NH50-': 'NH50 - रारा५०',
    'NH51-': 'NH51 - रारा५१',
    'NH52-': 'NH52 - रारा५२',
    'NH53-': 'NH53 - रारा५३',
    'NH54-': 'NH54 - रारा५४',
    'NH55-': 'NH55 - रारा५५',
    'NH56-': 'NH56 - रारा५६',
    'NH57-': 'NH57 - रारा५७',
    'NH58-': 'NH58 - रारा५८',
    'NH59-': 'NH59 - रारा५९',
    'NH60-': 'NH60 - रारा६०',
    'NH61-': 'NH61 - रारा६१',
    'NH62-': 'NH62 - रारा६२',
    'NH63-': 'NH63 - रारा६३',
    'NH64-': 'NH64 - रारा६४',
    'NH65-': 'NH65 - रारा६५',
    'NH66-': 'NH66 - रारा६६',
    'NH67-': 'NH67 - रारा६७',
    'NH68-': 'NH68 - रारा६८',
    'NH69-': 'NH69 - रारा६९',
    'NH70-': 'NH70 - रारा७०',
    'NH71-': 'NH71 - रारा७१',
    'NH72-': 'NH72 - रारा७२',
    'NH73-': 'NH73 - रारा७३',
    'NH74-': 'NH74 - रारा७४',
    'NH75-': 'NH75 - रारा७५',
    'NH76-': 'NH76 - रारा७६',
    'NH77-': 'NH77 - रारा७७',
    'NH78-': 'NH78 - रारा७८',
    'NH79-': 'NH79 - रारा७९',
    'NH80-': 'NH80 - रारा८०',
  };

  // Suffixes with No Standard Abbreviation
  const wmessa_knownNoAbbr = ['Lane', 'Loop', 'Mall', 'Mews', 'Path', 'Ramp', 'Rise', 'View', 'Walk', 'Way'];

  // --- NEW DATA FOR GENERAL WORDS (PRE-SUFFIX) ---
  // General Word Suggestion Data (WordToAbbreviate: Abbreviation)
  const wmessa_generalWordSuggestions = {
    Mount: 'Mt',
    Saint: 'St', // Note: "St" for Saint. Suffix logic handles "St" for Street.
    Fort: 'Ft',
    Marg: 'Marga', // Nepal: "Marg" should be expanded to full word "Marga"
    // Add other common words like "North": "N", "South": "S", etc., if standard for pre-suffix words.
  };

  // General Word Approved Abbreviation Data (Abbreviation: FullWord) - for validation
  const wmessa_generalWordApprovedAbbr = {
    Mt: 'Mount',
    St: 'Saint',
    Ft: 'Fort',
    Marga: 'Marg', // Nepal: "Marga" is the approved full word form
    // e.g. "N": "North", "S": "South"
  };

  function wmessa_titleCase(str) {
    return str
      .split(/\s+/)
      .map(function (txt) {
        // If word matches a preserve-case word (case-insensitive), use the preserved version
        const preserve = wmessa_preserveCaseWords.find((w) => w.toLowerCase() === txt.toLowerCase());
        if (preserve) return preserve;
        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
      })
      .join(' ');
  }

  let wmessa_valueObserver;

  // Add styles for sidebar panel
  function addSidebarStyles() {
    const styles = [
      '.rnh-container { margin: 10px 5px; }',
      '.rnh-title { font-weight: bold; margin-bottom: 10px; }',
      '.rnh-scan-counter { color: #666; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }',
      '.rnh-spinner { animation: rnh-spin 0.5s infinite linear; }',
      '@keyframes rnh-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }',
      '.rnh-progress-bar { width: 1%; height: 8px; background-color: #4CAF50; margin-bottom: 10px; border: 1px solid #333; transition: width 0.3s ease; display: none; }',
      '.rnh-results { max-height: 400px; overflow-y: auto; }',
      '.rnh-segment-item { padding: 8px; margin-bottom: 5px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; cursor: pointer; }',
      '.rnh-segment-item:hover { background: #f0f0f0; border-color: #999; }',
      '.rnh-segment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }',
      '.rnh-segment-id { font-size: 11px; color: #666; cursor: pointer; }',
      '.rnh-segment-id:hover { color: #0066cc; text-decoration: underline; }',
      '.rnh-road-type { font-size: 11px; color: #666; background: #e0e0e0; padding: 2px 6px; border-radius: 3px; }',
      '.rnh-name-row { margin-bottom: 3px; font-size: 12px; }',
      '.rnh-name-label { font-weight: bold; color: #555; min-width: 40px; display: inline-block; }',
      '.rnh-current-name { color: #d32f2f; }',
      '.rnh-suggested-name { color: #388e3c; }',
      '.rnh-fix-button { padding: 4px 8px; font-size: 11px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; }',
      '.rnh-fix-button:hover { background: #45a049; }',
      '.rnh-fix-button:disabled { background: #ccc; cursor: not-allowed; }',
      '.rnh-fix-all-button { width: 100%; padding: 8px; margin-bottom: 10px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }',
      '.rnh-fix-all-button:hover { background: #1976D2; }',
      '.rnh-fix-all-button:disabled { background: #ccc; cursor: not-allowed; }',
      '.rnh-no-issues { text-align: center; color: #4CAF50; padding: 20px; font-weight: bold; }',
      '.rnh-alt-names { margin-top: 5px; padding-left: 10px; border-left: 2px solid #ddd; }',
      '.rnh-complete-icon { color: #4CAF50; }',
      '.rnh-button-container { margin-bottom: 10px; }',
      '.rnh-preview-container { margin-bottom: 10px; display: flex; align-items: center; gap: 5px; }',
      '.rnh-preview-checkbox { margin: 0; }',
      '.rnh-preview-label { margin: 0; font-size: 13px; cursor: pointer; }'
    ].join('\n');
    
    GM_addStyle(styles);
  }

  function wmessa_init() {
    // Initialize sidebar panel
    initSidebarPanel();
    
    const observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.removedNodes.forEach((node) => {
            if (node.classList && node.classList.contains('address-edit-card')) {
              if (wmessa_valueObserver) {
                wmessa_valueObserver.disconnect();
              }
            }
          });

          mutation.addedNodes.forEach((node) => {
            if (node.classList && node.classList.contains('address-edit-card')) {
              setTimeout(() => {
                // Main street name
                const streetNameInput = node.querySelector('wz-autocomplete.street-name');
                if (streetNameInput && streetNameInput.shadowRoot) {
                  const wzTextInput = streetNameInput.shadowRoot.querySelector('wz-text-input');
                  if (wzTextInput) {
                    wmessa_monitor(wzTextInput);
                  } else {
                    console.warn('WMESSA: wz-text-input not found in street-name shadowRoot.');
                  }
                } else {
                  console.warn('WMESSA: street-name input or its shadowRoot not found.');
                }
                // Alt street name(s)
                const altStreetInputs = node.querySelectorAll('wz-autocomplete.alt-street-name');
                altStreetInputs.forEach((altInput) => {
                  if (altInput && altInput.shadowRoot) {
                    const altWzTextInput = altInput.shadowRoot.querySelector('wz-text-input');
                    if (altWzTextInput) {
                      wmessa_monitor(altWzTextInput);
                    } else {
                      console.warn('WMESSA: wz-text-input not found in alt-street-name shadowRoot.');
                    }
                  } else {
                    console.warn('WMESSA: alt-street-name input or its shadowRoot not found.');
                  }
                });
              }, 250);
            }
          });
        }
      });
    });
    const editPanel = document.getElementById('edit-panel');
    if (editPanel) {
      observer.observe(editPanel, { childList: true, subtree: true });
    } else {
      console.warn('WMESSA: Edit panel not found for observer.');
    }

    WazeWrap.Interface.ShowScriptUpdate('WME Road Name Helper NP', GM_info.script.version, updateMessage, GreasyFork_URL, forumURL);
  }

  // Also observe for alt street card (for alt names)
  const altStreetPanelObserver = new MutationObserver((mutationsList) => {
    mutationsList.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => {
          if (node.classList && node.classList.contains('edit-alt-street-card')) {
            setTimeout(() => {
              const altStreetInput = node.querySelector('wz-autocomplete.alt-street-name');
              if (altStreetInput && altStreetInput.shadowRoot) {
                const altWzTextInput = altStreetInput.shadowRoot.querySelector('wz-text-input');
                if (altWzTextInput) {
                  wmessa_monitor(altWzTextInput);
                } else {
                  console.warn('WMESSA: wz-text-input not found in alt-street-name shadowRoot (alt card).');
                }
              } else {
                console.warn('WMESSA: alt-street-name input or its shadowRoot not found (alt card).');
              }
            }, 250);
          }
        });
      }
    });
  });
  // Observe the whole document for alt street cards
  altStreetPanelObserver.observe(document.body, { childList: true, subtree: true });

  function wmessa_monitor(element) {
    let abbrContainer = document.createElement('div');
    abbrContainer.id = 'WMESSA_container';
    abbrContainer.innerHTML =
      '<div class="WMESSA_icon" title="WME Standard Suffix Abbreviations"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M4.5 2A2.5 2.5 0 0 0 2 4.5v2.879a2.5 2.5 0 0 0 .732 1.767l4.5 4.5a2.5 2.5 0 0 0 3.536 0l2.878-2.878a2.5 2.5 0 0 0 0-3.536l-4.5-4.5A2.5 2.5 0 0 0 7.38 2H4.5ZM5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" /></svg></div>' +
      '<div id="WMESSA_output">Loading...</div>';

    const statusTextContainer = element.shadowRoot.querySelector('.status-text-container');
    if (!statusTextContainer) {
      console.warn('WMESSA: .status-text-container not found. UI will not be displayed.');
      return;
    }
    statusTextContainer.insertBefore(abbrContainer, statusTextContainer.firstChild);

    let abbrOutput = abbrContainer.querySelector('#WMESSA_output');

    const css = [
      '.status-text-container {width: calc(100% + ' +
        (document.querySelector('#edit-panel .address-edit-card .street-name-row .tts-playback')
          ? document.querySelector('#edit-panel .address-edit-card .street-name-row .tts-playback').offsetWidth
          : 0) +
        'px); display: flex; flex-direction: column-reverse;}',
      '#WMESSA_container {display: flex; align-items: center; flex-grow: 1; margin-top: var(--wz-label-margin, 8px); padding: 0 2px; border-radius: 5px; background: #ffffff; color: #ffffff; gap: 5px; cursor: default; transition: background 0.25s linear, color 0.25s linear; font-size: 0.9em;}',
      '#WMESSA_output {color: #000000; white-space: pre-wrap; flex-grow: 1;}',
      '.WMESSA_icon {display: inline-flex; padding: 2px; height: 12px; background: rgba(0,0,0,0.5); border-radius: 3px; flex-shrink: 0; margin-right: 5px;}',
      '.WMESSA_icon svg {height: 100%;}',
      '#WMESSA_container.info {background: #e0f2fe; color: #e0f2fe;}',
      '#WMESSA_container.check {background: #fef3c7; color: #fef3c7; cursor: pointer;}',
      '#WMESSA_container.check:hover {background: #fde68a; color: #fde68a;}',
      '#WMESSA_container.valid {background: #d1fae5; color: #d1fae5;}',
    ].join(' ');
    const styleElement = document.createElement('style');
    styleElement.type = 'text/css';
    styleElement.textContent = css;
    element.shadowRoot.appendChild(styleElement);

    if (wmessa_valueObserver) {
      wmessa_valueObserver.disconnect();
    }
    wmessa_valueObserver = new MutationObserver((mutationsList, observer) => {
      for (let mutation of mutationsList) {
        if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
          wmessa_update(element, abbrContainer, abbrOutput);
        }
      }
    });
    wmessa_valueObserver.observe(element, { attributes: true });

    wmessa_update(element, abbrContainer, abbrOutput);

    // Add Tab key support for applying suggestion
    element.addEventListener('keydown', function (e) {
      if (e.key === 'Tab') {
        const abbrContainer = element.shadowRoot.querySelector('#WMESSA_container');
        if (abbrContainer && abbrContainer.classList.contains('check')) {
          const abbrOutput = abbrContainer.querySelector('#WMESSA_output');
          // Simulate click to apply suggestion
          abbrContainer.click();
          e.preventDefault();
        }
      }
    });
  }

  function wmessa_analyzeSuffix(suffix) {
    const suffixLower = suffix.toLowerCase();
    let result = { status: 'info', message: 'No match for suffix.', proposed: suffix, original: suffix };

    const isKnownNoAbbrExact = (sLower) => wmessa_knownNoAbbr.some((kna) => kna.toLowerCase() === sLower);
    const getKnownNoAbbrCased = (sLower) => wmessa_knownNoAbbr.find((kna) => kna.toLowerCase() === sLower);

    // 0. Exact match for highway abbreviations (special case)
    const hwyKey = Object.keys(wmessa_suggestedHwyAbbr).find((key) => key.toLowerCase() === suffixLower);
    if (hwyKey) {
      const suggestedHwy = wmessa_suggestedHwyAbbr[hwyKey];
      if (suggestedHwy.toLowerCase() !== suffixLower) {
        return { status: 'check', message: `Use ${suggestedHwy} for ${hwyKey}`, proposed: suggestedHwy, original: suffix };
      } else {
        return { status: 'valid', message: `${suggestedHwy}`, proposed: suggestedHwy, original: suffix };
      }
    }

    // 1. Exact match: Typed IS an approved abbreviation (e.g., "Rd")
    if (wmessa_approvedAbbr.hasOwnProperty(suffix)) {
      return { status: 'valid', message: `${suffix} for ${wmessa_approvedAbbr[suffix]}`, proposed: suffix, original: suffix };
    }
    const approvedKeyCi = Object.keys(wmessa_approvedAbbr).find((k) => k.toLowerCase() === suffixLower);
    if (approvedKeyCi) {
      return { status: 'valid', message: `${approvedKeyCi} for ${wmessa_approvedAbbr[approvedKeyCi]}`, proposed: approvedKeyCi, original: suffix };
    }

    // 2. Exact match: Typed IS a known non-abbreviated suffix (e.g., "Lane")
    if (isKnownNoAbbrExact(suffixLower)) {
      const casedNoAbbr = getKnownNoAbbrCased(suffixLower) || suffix;
      return { status: 'valid', message: casedNoAbbr, proposed: casedNoAbbr, original: suffix };
    }

    const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const suffixRegex = new RegExp(`^${escapedSuffix}`, 'i');

    // 3. Suggestion: Typed is (prefix of) a full word that should be abbreviated (e.g., "Street" or "Stre" -> "St")
    let suggestFromFullKey = Object.keys(wmessa_suggestedAbbr).find((key) => key.toLowerCase() === suffixLower);
    if (!suggestFromFullKey) {
      suggestFromFullKey = Object.keys(wmessa_suggestedAbbr).find((key) => suffixRegex.test(key));
    }
    if (suggestFromFullKey) {
      const suggestedAbbr = wmessa_suggestedAbbr[suggestFromFullKey];
      if (suggestedAbbr.toLowerCase() !== suffixLower) {
        return { status: 'check', message: `Use ${suggestedAbbr} for ${suggestFromFullKey}`, proposed: suggestedAbbr, original: suffix };
      } else {
        // Typed the same as the suggestion (e.g. typed "Lane", suggested "Lane" because "Ln":"Lane")
        if (isKnownNoAbbrExact(suggestedAbbr.toLowerCase())) {
          const casedNoAbbr = getKnownNoAbbrCased(suggestedAbbr.toLowerCase()) || suggestedAbbr;
          return { status: 'valid', message: casedNoAbbr, proposed: casedNoAbbr, original: suffix };
        }
        const finalApprovedKeyCi = Object.keys(wmessa_approvedAbbr).find((k) => k.toLowerCase() === suggestedAbbr.toLowerCase());
        if (finalApprovedKeyCi) {
          return { status: 'valid', message: `${finalApprovedKeyCi} for ${wmessa_approvedAbbr[finalApprovedKeyCi]}`, proposed: finalApprovedKeyCi, original: suffix };
        }
      }
    }

    // 4. Suggestion: Typed is (prefix of) a known non-abbreviated word (e.g., "Lan" -> "Lane")
    const knownNoAbbrCompletion = wmessa_knownNoAbbr.find((key) => suffixRegex.test(key));
    if (knownNoAbbrCompletion && knownNoAbbrCompletion.toLowerCase() !== suffixLower) {
      return { status: 'check', message: `Use ${knownNoAbbrCompletion}`, proposed: knownNoAbbrCompletion, original: suffix };
    }

    // 5. Suggestion: Typed is (prefix of) an approved abbreviation (e.g., "Al" -> "Ally")
    const approvedAbbrCompletionKey = Object.keys(wmessa_approvedAbbr).find((key) => suffixRegex.test(key));
    if (approvedAbbrCompletionKey && approvedAbbrCompletionKey.toLowerCase() !== suffixLower) {
      return { status: 'check', message: `Use ${approvedAbbrCompletionKey} for ${wmessa_approvedAbbr[approvedAbbrCompletionKey]}`, proposed: approvedAbbrCompletionKey, original: suffix };
    }

    return result;
  }

  function wmessa_update(element, abbrContainer, abbrOutput) {
    abbrContainer.classList.remove('valid', 'check', 'info');
    abbrContainer.onclick = null;
    abbrOutput.innerText = 'Awaiting input...';

    const currentValue = element.value.trim();
    if (!currentValue) {
      return;
    }

    if (currentValue.match(/^The [a-zA-Z0-9\s'-]+$/i) && currentValue.split(/\s+/).length <= 3) {
      // "The x" or "The x y"
      abbrContainer.classList.add('info');
      abbrOutput.innerText = "Do not abbreviate 'The x' names";
      return;
    }

    let currentWords = currentValue.split(/\s+/);
    let proposedWords = [...currentWords];
    let preSuffixChangesMade = false;
    let overallStatus = 'info';
    let messages = [];

    // --- Process Pre-Suffix Words ---
    if (currentWords.length > 1) {
      for (let i = 0; i < currentWords.length - 1; i++) {
        const word = currentWords[i];
        const wordLower = word.toLowerCase();

        const generalApprovedKeyCi = Object.keys(wmessa_generalWordApprovedAbbr).find((k) => k.toLowerCase() === wordLower);
        if (generalApprovedKeyCi) {
          // Word is already an approved general abbreviation
          if (word !== generalApprovedKeyCi) {
            // Correct casing if needed
            proposedWords[i] = generalApprovedKeyCi;
            preSuffixChangesMade = true;
          }
          continue;
        }

        const generalSuggestionKeyCi = Object.keys(wmessa_generalWordSuggestions).find((k) => k.toLowerCase() === wordLower);
        if (generalSuggestionKeyCi) {
          const suggestedGeneralAbbr = wmessa_generalWordSuggestions[generalSuggestionKeyCi];
          if (suggestedGeneralAbbr.toLowerCase() !== wordLower) {
            proposedWords[i] = suggestedGeneralAbbr;
            preSuffixChangesMade = true;
          }
        }
      }
    }

    // --- Process Suffix (last word) ---
    let suffixAnalysis = {
      status: 'info',
      message: currentWords.length > 0 ? 'Awaiting valid suffix.' : 'Awaiting input.',
      proposed: currentWords.length > 0 ? currentWords[currentWords.length - 1] : '',
      original: currentWords.length > 0 ? currentWords[currentWords.length - 1] : '',
    };
    if (currentWords.length > 0) {
      const potentialSuffix = currentWords[currentWords.length - 1];
      suffixAnalysis = wmessa_analyzeSuffix(potentialSuffix);
      proposedWords[currentWords.length - 1] = suffixAnalysis.proposed;
    }

    // --- Combine Results and Determine UI ---
    const finalProposedString = wmessa_titleCase(proposedWords.join(' '));
    const suffixChanged = currentWords.length > 0 && suffixAnalysis.proposed.toLowerCase() !== suffixAnalysis.original.toLowerCase();
    const capitalizationChanged = currentValue !== finalProposedString;
    let anyChangeProposed = preSuffixChangesMade || suffixChanged || capitalizationChanged;

    if (anyChangeProposed) {
      overallStatus = 'check';
      let suggestionDetails = [];
      if (preSuffixChangesMade) suggestionDetails.push('word(s) before suffix');
      if (suffixChanged) suggestionDetails.push('suffix');

      messages.push(`Suggest: "${finalProposedString}"`);
      if (suggestionDetails.length > 0) {
        messages.push(`(Changes to ${suggestionDetails.join(' & ')})`);
      }
      if (suffixChanged && suffixAnalysis.message && suffixAnalysis.message.startsWith('Use ')) {
        messages.push(suffixAnalysis.message); // Add specific suffix suggestion message
      }

      abbrContainer.onclick = function () {
        element.value = finalProposedString;
      };
    } else {
      // No changes proposed, evaluate if current input is valid or just no rules hit
      let allWordsAreStandard = true; // Assume true unless a known full word (that can be abbreviated) is found
      if (currentWords.length > 1) {
        for (let i = 0; i < currentWords.length - 1; i++) {
          const word = currentWords[i];
          const wordLower = word.toLowerCase();
          // Check if it's a full word that *could* be abbreviated, but isn't
          const canBeAbbreviatedKey = Object.keys(wmessa_generalWordSuggestions).find((k) => k.toLowerCase() === wordLower);
          // And it's not already an abbreviation of itself or something else
          const isAbbreviation = Object.keys(wmessa_generalWordApprovedAbbr).find((k) => k.toLowerCase() === wordLower);
          if (canBeAbbreviatedKey && !isAbbreviation) {
            allWordsAreStandard = false;
            break;
          }
        }
      }

      if (suffixAnalysis.status === 'valid' && allWordsAreStandard) {
        overallStatus = 'valid';
        messages.push(suffixAnalysis.message || `"${currentValue}" is standard.`);
      } else {
        overallStatus = 'info'; // Default to info if not perfectly valid or no suggestions
        if (!allWordsAreStandard) {
          messages.push('Consider standard abbreviations for words before suffix.');
        }
        if (suffixAnalysis.status === 'info') {
          messages.push(suffixAnalysis.message || 'Check suffix standards.');
        } else if (suffixAnalysis.status === 'valid' && !allWordsAreStandard) {
          // Suffix is fine, but pre-words are not optimal
          messages.push(`Suffix "${suffixAnalysis.proposed}" is standard.`);
        } else {
          messages.push(suffixAnalysis.message || `Review standards for "${currentValue}"`);
        }
      }
    }

    abbrContainer.classList.add(overallStatus);
    abbrOutput.innerText = messages.filter((m) => m).join('\n') || 'Awaiting input or check standards.';
  }

  // ========== SIDEBAR PANEL FUNCTIONS ==========
  
  async function initSidebarPanel() {
    try {
      addSidebarStyles();
      
      const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
      
      // Set tab label
      tabLabel.textContent = 'RNH';
      tabLabel.title = 'Road Name Helper';
      
      // Create main container
      const container = document.createElement('div');
      container.className = 'rnh-container';
      
      // Title
      const title = document.createElement('wz-overline');
      title.textContent = 'Road Name Helper';
      title.style.marginBottom = '10px';
      container.appendChild(title);
      
      // Version
      const version = document.createElement('wz-label');
      version.textContent = `Version ${SCRIPT_VERSION}`;
      version.style.marginBottom = '10px';
      container.appendChild(version);
      
      // Preview checkbox
      const previewContainer = document.createElement('div');
      previewContainer.className = 'rnh-preview-container';
      const previewCheckbox = document.createElement('input');
      previewCheckbox.type = 'checkbox';
      previewCheckbox.id = 'rnh-preview-checkbox';
      previewCheckbox.className = 'rnh-preview-checkbox';
      previewCheckbox.checked = previewEnabled;
      previewCheckbox.onchange = onPreviewChanged;
      const previewLabel = document.createElement('label');
      previewLabel.htmlFor = 'rnh-preview-checkbox';
      previewLabel.className = 'rnh-preview-label';
      previewLabel.textContent = 'Preview (highlight segments with issues)';
      previewContainer.appendChild(previewCheckbox);
      previewContainer.appendChild(previewLabel);
      container.appendChild(previewContainer);
      
      // Scan counter
      const scanCounter = document.createElement('div');
      scanCounter.className = 'rnh-scan-counter';
      scanCounter.innerHTML = '<i class="fa fa-check-circle rnh-complete-icon"></i><span>Scanned: 0 segments</span>';
      container.appendChild(scanCounter);
      cachedElements.scanCounter = scanCounter;
      
      // Progress bar
      const progressBar = document.createElement('div');
      progressBar.className = 'rnh-progress-bar';
      container.appendChild(progressBar);
      cachedElements.progressBar = progressBar;
      
      // Fix All button
      const buttonContainer = document.createElement('div');
      buttonContainer.className = 'rnh-button-container';
      const fixAllButton = document.createElement('button');
      fixAllButton.className = 'rnh-fix-all-button';
      fixAllButton.textContent = 'Fix All Names';
      fixAllButton.disabled = true;
      fixAllButton.onclick = fixAllNames;
      buttonContainer.appendChild(fixAllButton);
      container.appendChild(buttonContainer);
      cachedElements.fixAllButton = fixAllButton;
      
      // Results container
      const resultsContainer = document.createElement('div');
      resultsContainer.className = 'rnh-results';
      container.appendChild(resultsContainer);
      cachedElements.resultsContainer = resultsContainer;
      
      tabPane.appendChild(container);
      
      // Register event handlers and store subscriptions for cleanup
      eventSubscriptions.push(
        sdk.Events.on({
          eventName: 'wme-map-move-end',
          eventHandler: debouncedScan
        })
      );
      
      eventSubscriptions.push(
        sdk.Events.on({
          eventName: 'wme-map-zoom-changed',
          eventHandler: debouncedScan
        })
      );
      
      eventSubscriptions.push(
        sdk.Events.on({
          eventName: 'wme-after-edit',
          eventHandler: debouncedScan
        })
      );
      
      // Initial scan
      debouncedScan();
      
      // Add cleanup on page unload
      window.addEventListener('beforeunload', cleanup);
      
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error initializing sidebar panel:`, error);
    }
  }
  
  /**
   * Cleanup function to unsubscribe from events and clear timers
   */
  function cleanup() {
    console.log(`${SCRIPT_NAME}: Cleaning up...`);
    
    // Clear any pending scan timeout
    if (scanTimeout) {
      clearTimeout(scanTimeout);
    }
    
    // Unsubscribe from all events
    eventSubscriptions.forEach(subscription => {
      try {
        if (subscription && typeof subscription.unsubscribe === 'function') {
          subscription.unsubscribe();
        }
      } catch (err) {
        console.warn(`${SCRIPT_NAME}: Error unsubscribing from event:`, err);
      }
    });
    eventSubscriptions = [];
  }
  
  /**
   * Debounced scan function to prevent excessive scanning during map movements
   */
  function debouncedScan() {
    clearTimeout(scanTimeout);
    scanTimeout = setTimeout(() => {
      if (!isScanning) {
        scanRoadNames();
      }
    }, SCAN_DEBOUNCE_DELAY);
  }
  
  function onScreen(geometry) {
    if (!geometry || !currentMapExtent) return false;
    
    const [left, bottom, right, top] = currentMapExtent;
    
    if (geometry.type === 'Point') {
      const [lon, lat] = geometry.coordinates;
      return lon >= left && lon <= right && lat >= bottom && lat <= top;
    } else if (geometry.type === 'LineString') {
      return geometry.coordinates.some(([lon, lat]) => 
        lon >= left && lon <= right && lat >= bottom && lat <= top
      );
    }
    return true;
  }
  
  /**
   * Scan all on-screen road segments for naming issues
   */
  async function scanRoadNames() {
    if (isScanning) {
      console.log(`${SCRIPT_NAME}: Scan already in progress, skipping...`);
      return;
    }
    
    isScanning = true;
    try {
      // Get current map extent
      currentMapExtent = sdk.Map.getMapExtent();
      
      if (!currentMapExtent) {
        throw new Error('Unable to get map extent');
      }
      
      // Show progress
      updateProgress(true, 1);
      
      scannedSegments = [];
      const segments = sdk.DataModel.Segments.getAll();
      const onScreenSegments = segments.filter(segment => onScreen(segment.geometry));
      
      console.log(`${SCRIPT_NAME}: Scanning ${onScreenSegments.length} on-screen segments...`);
      
      let processedCount = 0;
      let editableCount = 0;
      const totalCount = onScreenSegments.length;
      
      for (const segment of onScreenSegments) {
        processedCount++;
        
        // Check if segment is editable
        if (!sdk.DataModel.Segments.hasPermissions({
          permission: 'EDIT_PROPERTIES',
          segmentId: segment.id
        })) {
          continue;
        }
        
        editableCount++;
        const issues = [];
        
        // Get street names from Streets data model using street IDs
        let primaryStreetName = '';
        const altStreetNames = [];
        
        if (segment.primaryStreetId) {
          try {
            const primaryStreet = sdk.DataModel.Streets.getById({ streetId: segment.primaryStreetId });
            if (primaryStreet?.name) {
              primaryStreetName = primaryStreet.name;
              
              const analysis = analyzeStreetName(primaryStreet.name);
              if (analysis.needsFix) {
                issues.push({
                  type: 'primary',
                  current: primaryStreet.name,
                  suggested: analysis.suggested,
                  reason: analysis.reason
                });
              }
            }
          } catch (err) {
            console.warn(`${SCRIPT_NAME}: Error getting primary street ${segment.primaryStreetId}:`, err);
          }
        }
        
        // Check alternate street names
        if (segment.alternateStreetIds && segment.alternateStreetIds.length > 0) {
          segment.alternateStreetIds.forEach((streetId, index) => {
            try {
              const altStreet = sdk.DataModel.Streets.getById({ streetId });
              if (altStreet?.name) {
                altStreetNames.push(altStreet.name);
                
                const analysis = analyzeStreetName(altStreet.name);
                if (analysis.needsFix) {
                  issues.push({
                    type: 'alt',
                    index: index,
                    current: altStreet.name,
                    suggested: analysis.suggested,
                    reason: analysis.reason
                  });
                }
              }
            } catch (err) {
              console.warn(`${SCRIPT_NAME}: Error getting alt street ${streetId}:`, err);
            }
          });
        }
        
        if (issues.length > 0) {
          scannedSegments.push({
            id: segment.id,
            roadType: segment.roadType,
            primaryStreetId: segment.primaryStreetId,
            alternateStreetIds: segment.alternateStreetIds || [],
            primaryStreetName: primaryStreetName,
            altStreetNames: altStreetNames,
            issues: issues
          });
        }
        
        // Update progress periodically
        if (processedCount % PROGRESS_UPDATE_THROTTLE === 0 || processedCount === totalCount) {
          updateProgress(true, (processedCount / totalCount) * 100);
          updateScanCounter(processedCount);
        }
      }
      
      console.log(`${SCRIPT_NAME}: Scan complete. Total: ${totalCount}, Editable: ${editableCount}, Issues found: ${scannedSegments.length}`);
      
      // Update UI
      updateProgress(false);
      updateScanCounter(totalCount);
      displayResults();
      
      // Highlight segments if preview is enabled
      if (previewEnabled) {
        highlightSegments();
      }
      
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error scanning road names:`, error);
      updateProgress(false);
      
      // Show error message to user
      if (cachedElements.resultsContainer) {
        cachedElements.resultsContainer.innerHTML = `<div style="text-align: center; color: #d32f2f; padding: 20px;">⚠️ Error scanning segments. Please try again.</div>`;
      }
    } finally {
      isScanning = false;
    }
  }
  
  /**
   * Analyze a street name and determine if it needs fixing
   * @param {string} streetName - The street name to analyze
   * @returns {Object} Analysis result with needsFix, suggested, and reason properties
   */
  function analyzeStreetName(streetName) {
    if (!streetName) return { needsFix: false };
    
    const currentWords = streetName.split(/\s+/);
    let proposedWords = [...currentWords];
    let changed = false;
    let reasons = [];
    
    // Check for highway patterns first (e.g., "NH41 - रा४१" should become "NH41 - रारा४१")
    const hwyPattern = /^(NH\d{2})\s*-\s*(.+)$/i;
    const hwyMatch = streetName.match(hwyPattern);
    if (hwyMatch) {
      const hwyCode = hwyMatch[1];
      const hwyPart = hwyMatch[2].trim();
      
      // Preserve highway names ending with "Hwy" (e.g., "NH41 - Prithvi Hwy")
      if (hwyPart.match(/\s+Hwy$/i)) {
        return { needsFix: false };
      }
      
      // Check if the highway part needs fixing
      const hwyKey = `${hwyCode.toUpperCase()}-`;
      const suggestedHwy = wmessa_suggestedHwyAbbr[hwyKey];
      
      if (suggestedHwy && streetName !== suggestedHwy) {
        return {
          needsFix: true,
          suggested: suggestedHwy,
          reason: `Highway format: ${streetName} → ${suggestedHwy}`
        };
      }
      
      // Also check if just the Devanagari part needs fixing
      const devanagariSuggestion = wmessa_suggestedAbbr[hwyPart];
      if (devanagariSuggestion) {
        const newSuggestion = `${hwyCode.toUpperCase()} - ${devanagariSuggestion}`;
        if (streetName !== newSuggestion) {
          return {
            needsFix: true,
            suggested: newSuggestion,
            reason: `Highway Devanagari: ${hwyPart} → ${devanagariSuggestion}`
          };
        }
      }
    }
    
    // Check for standalone Devanagari abbreviations in any position
    for (let i = 0; i < currentWords.length; i++) {
      const word = currentWords[i];
      const devanagariSuggestion = wmessa_suggestedAbbr[word];
      if (devanagariSuggestion && devanagariSuggestion !== word) {
        proposedWords[i] = devanagariSuggestion;
        changed = true;
        reasons.push(`${word} → ${devanagariSuggestion}`);
      }
    }
    
    // Process pre-suffix words
    if (currentWords.length > 1) {
      for (let i = 0; i < currentWords.length - 1; i++) {
        const word = currentWords[i];
        const wordLower = word.toLowerCase();
        
        // Skip if already processed as Devanagari
        if (proposedWords[i] !== currentWords[i]) continue;
        
        const generalSuggestionKeyCi = Object.keys(wmessa_generalWordSuggestions).find((k) => k.toLowerCase() === wordLower);
        if (generalSuggestionKeyCi) {
          const suggestedGeneralAbbr = wmessa_generalWordSuggestions[generalSuggestionKeyCi];
          if (suggestedGeneralAbbr.toLowerCase() !== wordLower) {
            proposedWords[i] = suggestedGeneralAbbr;
            changed = true;
            reasons.push(`${word} → ${suggestedGeneralAbbr}`);
          }
        }
      }
    }
    
    // Process suffix (last word) - only if not already processed
    if (currentWords.length > 0 && proposedWords[proposedWords.length - 1] === currentWords[currentWords.length - 1]) {
      const potentialSuffix = currentWords[currentWords.length - 1];
      const suffixAnalysis = wmessa_analyzeSuffix(potentialSuffix);
      
      if (suffixAnalysis.status === 'check' && suffixAnalysis.proposed.toLowerCase() !== potentialSuffix.toLowerCase()) {
        proposedWords[currentWords.length - 1] = suffixAnalysis.proposed;
        changed = true;
        reasons.push(suffixAnalysis.message || `${potentialSuffix} → ${suffixAnalysis.proposed}`);
      }
    }
    
    const finalProposed = wmessa_titleCase(proposedWords.join(' '));
    const capitalizationChanged = streetName !== finalProposed;
    
    if (changed || capitalizationChanged) {
      return {
        needsFix: true,
        suggested: finalProposed,
        reason: reasons.length > 0 ? reasons.join(', ') : 'Capitalization'
      };
    }
    
    return { needsFix: false };
  }
  
  function updateProgress(show, percent = 1) {
    const progressBar = cachedElements.progressBar;
    const scanCounter = cachedElements.scanCounter;
    
    if (!progressBar || !scanCounter) return;
    
    const spinner = scanCounter.querySelector('i');
    
    if (show) {
      progressBar.style.display = 'block';
      progressBar.style.width = Math.max(percent, 1) + '%';
      if (spinner) {
        spinner.className = 'fa fa-spinner rnh-spinner';
      }
    } else {
      progressBar.style.display = 'none';
      if (spinner) {
        spinner.className = 'fa fa-check-circle rnh-complete-icon';
      }
    }
  }
  
  function updateScanCounter(count) {
    const scanCounter = cachedElements.scanCounter;
    if (!scanCounter) return;
    
    const span = scanCounter.querySelector('span');
    if (span) {
      span.textContent = `Scanned: ${count} segments`;
    }
  }
  
  /**
   * Display scan results in the sidebar
   */
  function displayResults() {
    const resultsContainer = cachedElements.resultsContainer;
    const fixAllButton = cachedElements.fixAllButton;
    
    if (!resultsContainer) return;
    
    resultsContainer.innerHTML = '';
    
    if (scannedSegments.length === 0) {
      resultsContainer.innerHTML = '<div class="rnh-no-issues">✓ No issues found!</div>';
      if (fixAllButton) fixAllButton.disabled = true;
      return;
    }
    
    if (fixAllButton) fixAllButton.disabled = false;
    
    // Limit displayed segments for performance
    const displaySegments = scannedSegments.slice(0, MAX_SEGMENTS_TO_DISPLAY);
    const hiddenCount = scannedSegments.length - displaySegments.length;
    
    if (hiddenCount > 0) {
      const warningDiv = document.createElement('div');
      warningDiv.style.cssText = 'text-align: center; color: #ff9800; padding: 10px; background: #fff3e0; margin-bottom: 10px; border-radius: 4px; font-size: 12px;';
      warningDiv.textContent = `⚠️ Showing ${displaySegments.length} of ${scannedSegments.length} segments (limited for performance). Fix these first, then rescan.`;
      resultsContainer.appendChild(warningDiv);
    }
    
    displaySegments.forEach(segment => {
      const item = document.createElement('div');
      item.className = 'rnh-segment-item';
      
      // Add hover event listeners for highlighting
      item.onmouseenter = () => highlightSegmentOnHover(segment.id);
      item.onmouseleave = () => clearHoverHighlight();
      
      // Header with segment ID and road type
      const header = document.createElement('div');
      header.className = 'rnh-segment-header';
      
      const segmentId = document.createElement('span');
      segmentId.className = 'rnh-segment-id';
      segmentId.textContent = `Segment #${segment.id}`;
      segmentId.title = 'Click to select segment';
      segmentId.onclick = () => selectSegment(segment.id);
      
      const roadType = document.createElement('span');
      roadType.className = 'rnh-road-type';
      const roadTypeObj = sdk.DataModel.Segments.getRoadTypes().find(rt => rt.id === segment.roadType);
      roadType.textContent = roadTypeObj ? roadTypeObj.localizedName : 'Unknown';
      
      header.appendChild(segmentId);
      header.appendChild(roadType);
      item.appendChild(header);
      
      // Display issues
      segment.issues.forEach(issue => {
        const nameRow = document.createElement('div');
        nameRow.className = 'rnh-name-row';
        
        const label = document.createElement('span');
        label.className = 'rnh-name-label';
        label.textContent = issue.type === 'primary' ? 'Name:' : `Alt ${issue.index + 1}:`;
        
        const current = document.createElement('span');
        current.className = 'rnh-current-name';
        current.textContent = issue.current;
        
        const arrow = document.createElement('span');
        arrow.textContent = ' → ';
        
        const suggested = document.createElement('span');
        suggested.className = 'rnh-suggested-name';
        suggested.textContent = issue.suggested;
        
        nameRow.appendChild(label);
        nameRow.appendChild(current);
        nameRow.appendChild(arrow);
        nameRow.appendChild(suggested);
        
        item.appendChild(nameRow);
      });
      
      // Fix button
      const fixButton = document.createElement('button');
      fixButton.className = 'rnh-fix-button';
      fixButton.textContent = 'Fix';
      fixButton.onclick = () => fixSegmentNames(segment);
      item.appendChild(fixButton);
      
      resultsContainer.appendChild(item);
    });
  }
  
  async function selectSegment(segmentId) {
    try {
      const segment = sdk.DataModel.Segments.getById({ segmentId: segmentId });
      if (!segment) {
        console.warn(`${SCRIPT_NAME}: Segment ${segmentId} not found`);
        WazeWrap.Alerts.info(SCRIPT_NAME, `Segment ${segmentId} not found. It may have been deleted.`);
        return;
      }
      
      // First, select the segment in WME
      sdk.Editing.setSelection({ selection: { ids: [segmentId], objectType: 'segment' } });
      
      // Then try to center the map on it
      if (!segment.geometry || !segment.geometry.coordinates) {
        console.warn(`${SCRIPT_NAME}: Segment ${segmentId} has no geometry, but selected in editor`);
        return;
      }
      
      // Calculate center of segment geometry
      if (segment.geometry.type === 'LineString' && segment.geometry.coordinates.length > 0) {
        const coords = segment.geometry.coordinates;
        
        // Validate the coordinates array
        if (!coords || coords.length === 0) {
          console.warn(`${SCRIPT_NAME}: Segment ${segmentId} has empty coordinates array`);
          return;
        }
        
        const midIndex = Math.floor(coords.length / 2);
        const centerPoint = coords[midIndex];
        
        // Validate coordinates
        if (!centerPoint || !Array.isArray(centerPoint) || centerPoint.length < 2) {
          console.error(`${SCRIPT_NAME}: Invalid coordinate format for segment ${segmentId}:`, centerPoint);
          return;
        }
        
        // Ensure coordinates are numbers (geometry coordinates might be strings)
        const lon = Number(centerPoint[0]);
        const lat = Number(centerPoint[1]);
        
        // Validate lon/lat are valid numbers
        if (isNaN(lon) || isNaN(lat)) {
          console.error(`${SCRIPT_NAME}: Invalid lon/lat values for segment ${segmentId}: lon=${centerPoint[0]}, lat=${centerPoint[1]}`);
          return;
        }
        
        console.log(`${SCRIPT_NAME}: Centering map on segment ${segmentId} at [${lon}, ${lat}]`);
        sdk.Map.setMapCenter({ lonLat: { lon, lat } });
        
        // Zoom to a good level to see the segment
        const currentZoom = sdk.Map.getZoomLevel();
        if (currentZoom < 4) {
          sdk.Map.setZoomLevel({ zoomLevel: 4 });
        }
      }
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error selecting segment ${segmentId}:`, error);
      // Don't show alert here since the segment is likely still selected in the editor
    }
  }
  
  /**
   * Fix naming issues for a single segment
   * @param {Object} segment - Segment object with issues to fix
   */
  async function fixSegmentNames(segment) {
    try {
      // Get the current city from the segment's existing streets (preserve city)
      const getCityForStreet = (streetId) => {
        if (!streetId) return null;
        const street = sdk.DataModel.Streets.getById({ streetId });
        return street && street.cityId ? street.cityId : null;
      };
      
      let newPrimaryStreetId = null;
      const altStreetUpdates = {}; // Only store streets that need updating
      let hasChanges = false;
      
      // Process each issue
      segment.issues.forEach(issue => {
        if (issue.type === 'primary') {
          // Get the city from current primary street to preserve it
          const cityId = getCityForStreet(segment.primaryStreetId);
          if (!cityId) {
            console.warn(`${SCRIPT_NAME}: Cannot determine city for primary street, skipping`);
            return;
          }
          
          // Get or create street with suggested name in the same city
          let street = sdk.DataModel.Streets.getStreet({ 
            cityId: cityId, 
            streetName: issue.suggested 
          });
          
          if (!street) {
            street = sdk.DataModel.Streets.addStreet({ 
              streetName: issue.suggested, 
              cityId: cityId 
            });
          }
          
          newPrimaryStreetId = street.id;
          hasChanges = true;
          console.log(`${SCRIPT_NAME}: Primary street "${issue.current}" → "${issue.suggested}" (ID: ${street.id})`);
          
        } else if (issue.type === 'alt') {
          // Get the city from current alt street to preserve it
          const currentAltStreetId = segment.alternateStreetIds[issue.index];
          const cityId = getCityForStreet(currentAltStreetId);
          if (!cityId) {
            console.warn(`${SCRIPT_NAME}: Cannot determine city for alt street ${issue.index}, skipping`);
            return;
          }
          
          // Get or create alt street with suggested name in the same city
          let altStreet = sdk.DataModel.Streets.getStreet({ 
            cityId: cityId, 
            streetName: issue.suggested 
          });
          
          if (!altStreet) {
            altStreet = sdk.DataModel.Streets.addStreet({ 
              streetName: issue.suggested, 
              cityId: cityId 
            });
          }
          
          altStreetUpdates[issue.index] = altStreet.id;
          hasChanges = true;
          console.log(`${SCRIPT_NAME}: Alt street ${issue.index} "${issue.current}" → "${issue.suggested}" (ID: ${altStreet.id})`);
        }
      });
      
      // Only update if there are changes
      if (hasChanges) {
        // Build update parameters - only include what actually changed
        const updateParams = {
          segmentId: segment.id
        };
        
        // Only include primaryStreetId if it changed
        if (newPrimaryStreetId) {
          updateParams.primaryStreetId = newPrimaryStreetId;
        }
        
        // Only include alternateStreetIds if at least one alt street changed
        if (Object.keys(altStreetUpdates).length > 0) {
          const newAlternateStreetIds = [...(segment.alternateStreetIds || [])];
          Object.keys(altStreetUpdates).forEach(index => {
            newAlternateStreetIds[parseInt(index)] = altStreetUpdates[index];
          });
          updateParams.alternateStreetIds = newAlternateStreetIds;
        }
        
        console.log(`${SCRIPT_NAME}: Updating segment ${segment.id}:`, updateParams);
        await sdk.DataModel.Segments.updateAddress(updateParams);
        console.log(`${SCRIPT_NAME}: Successfully updated segment ${segment.id}`);
      } else {
        console.warn(`${SCRIPT_NAME}: No changes to apply for segment ${segment.id}`);
      }
      
      // Clear highlight and rescan after update
      if (previewEnabled) {
        sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
      }
      setTimeout(() => debouncedScan(), RESCAN_DELAY_AFTER_FIX);
      
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error fixing segment ${segment.id}:`, error);
      WazeWrap.Alerts.error(SCRIPT_NAME, `Failed to update segment ${segment.id}. Check console for details.`);
    }
  }
  
  /**
   * Fix naming issues for all scanned segments
   */
  async function fixAllNames() {
    const fixAllButton = cachedElements.fixAllButton;
    
    try {
      updateProgress(true, 1);
      if (fixAllButton) {
        fixAllButton.disabled = true;
        fixAllButton.textContent = 'Fixing...';
      }
      
      let processed = 0;
      let failed = 0;
      const total = scannedSegments.length;
      
      for (const segment of scannedSegments) {
        try {
          await fixSegmentNames(segment);
          processed++;
        } catch (err) {
          failed++;
          console.error(`${SCRIPT_NAME}: Failed to fix segment ${segment.id}:`, err);
        }
        updateProgress(true, (processed / total) * 100);
      }
      
      updateProgress(false);
      
      if (failed > 0) {
        console.warn(`${SCRIPT_NAME}: Fixed ${processed} segments, ${failed} failed`);
      }
      
      // Rescan to update results
      setTimeout(() => {
        debouncedScan();
      }, RESCAN_DELAY_AFTER_FIX);
      
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error fixing all names:`, error);
      updateProgress(false);
    } finally {
      if (fixAllButton) {
        fixAllButton.textContent = 'Fix All Names';
      }
    }
  }

  /**
   * Initialize the highlight layer for previewing segments
   */
  function initLayer() {
    try {
      sdk.Map.addLayer({
        layerName: LAYER_NAME,
        styleRules: [
          {
            style: {
              strokeColor: '#ff8800',
              strokeDashstyle: 'solid',
              strokeWidth: 35
            }
          }
        ]
      });
      
      const zIndex = sdk.Map.getLayerZIndex({ layerName: 'roads' }) - 3;
      sdk.Map.setLayerZIndex({ layerName: LAYER_NAME, zIndex });
      sdk.Map.setLayerOpacity({ layerName: LAYER_NAME, opacity: 0.6 });
      
      // HACK to prevent layer z-index from drifting above roads layer
      const checkLayerZIndex = () => {
        const currentZIndex = sdk.Map.getLayerZIndex({ layerName: LAYER_NAME });
        if (currentZIndex !== zIndex) {
          sdk.Map.setLayerZIndex({ layerName: LAYER_NAME, zIndex });
        }
      };
      setInterval(() => { checkLayerZIndex(); }, 100);
      // END HACK
      
      console.log(`${SCRIPT_NAME}: Highlight layer initialized`);
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error initializing layer:`, error);
    }
  }
  
  /**
   * Highlight a single segment on hover
   * @param {number} segmentId - Segment ID to highlight
   */
  function highlightSegmentOnHover(segmentId) {
    try {
      const segment = sdk.DataModel.Segments.getById({ segmentId });
      if (!segment || !segment.geometry) return;
      
      const features = [{
        type: 'Feature',
        id: 0,
        geometry: segment.geometry
      }];
      
      sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
      sdk.Map.addFeaturesToLayer({ layerName: LAYER_NAME, features });
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error highlighting segment on hover:`, error);
    }
  }
  
  /**
   * Clear hover highlight and restore preview highlights if enabled
   */
  function clearHoverHighlight() {
    if (previewEnabled) {
      // Restore all highlights if preview is enabled
      highlightSegments();
    } else {
      // Clear all highlights
      sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
    }
  }
  
  /**
   * Highlight segments with issues on the map
   */
  function highlightSegments() {
    try {
      if (!previewEnabled) {
        sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
        return;
      }
      
      const features = scannedSegments.map(segment => {
        const seg = sdk.DataModel.Segments.getById({ segmentId: segment.id });
        return {
          type: 'Feature',
          id: 0,
          geometry: seg.geometry
        };
      });
      
      sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
      sdk.Map.addFeaturesToLayer({ layerName: LAYER_NAME, features });
      
      console.log(`${SCRIPT_NAME}: Highlighted ${features.length} segments`);
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error highlighting segments:`, error);
    }
  }
  
  /**
   * Handle preview checkbox change
   */
  function onPreviewChanged(event) {
    previewEnabled = event.target.checked;
    console.log(`${SCRIPT_NAME}: Preview ${previewEnabled ? 'enabled' : 'disabled'}`);
    
    if (previewEnabled) {
      highlightSegments();
    } else {
      sdk.Map.removeAllFeaturesFromLayer({ layerName: LAYER_NAME });
    }
  }
  
  function wmessa_bootstrap() {
    const wmeSdk = getWmeSdk({ scriptId: 'wme-road-name-helper-np', scriptName: 'WME Road Name Helper NP' });
    sdk = wmeSdk;
    sdk.Events.once({ eventName: 'wme-ready' }).then(() => {
      loadScriptUpdateMonitor();
      initLayer();
      wmessa_init();
    });
  }

  function waitForWME() {
    if (!unsafeWindow.SDK_INITIALIZED) {
      setTimeout(waitForWME, 500);
      return;
    }
    unsafeWindow.SDK_INITIALIZED.then(wmessa_bootstrap);
  }
  waitForWME();

  function loadScriptUpdateMonitor() {
    try {
      const updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(SCRIPT_NAME, SCRIPT_VERSION, DOWNLOAD_URL, GM_xmlhttpRequest);
      updateMonitor.start();
    } catch (ex) {
      // Report, but don't stop if ScriptUpdateMonitor fails.
      console.error(`${SCRIPT_NAME}:`, ex);
    }
  }

  /*
Changelog:
2025.12.02.01
- Added sidebar panel "RNH" (Road Name Helper) for scanning road names
- Automatically scan on-screen segments while panning around
- Display road names with issues including alt names
- One-click fix for individual segments or all at once
- Added Devanagari number mappings from रा१० through रा८०

2025.06.18.01
- Restored AH02 to AH2.

2025.06.15.02
- Added: Typing "AH2" now suggests "AH02"
- Added: Typing "NH01 - रा१" or using suffix "रा१" now suggests "NH01 - रारा०१"
- Improved: Custom suffix and highway code mapping logic for Nepali and highway abbreviations
- Fixed: Suffix suggestion logic for Nepali abbreviations
*/
})();