function _upgradeTags()

in modules/services/nsi.js [400:587]


function _upgradeTags(tags, loc) {
  let newTags = Object.assign({}, tags);  // shallow copy
  let changed = false;

  // Before anything, perform trivial Wikipedia/Wikidata replacements
  Object.keys(newTags).forEach(osmkey => {
    const matchTag = osmkey.match(/^(\w+:)?wikidata$/);
    if (matchTag) {                         // Look at '*:wikidata' tags
      const prefix = (matchTag[1] || '');
      const wd = newTags[osmkey];
      const replace = _nsi.replacements[wd];    // If it matches a QID in the replacement list...

      if (replace && replace.wikidata !== undefined) {   // replace or delete `*:wikidata` tag
        changed = true;
        if (replace.wikidata) {
          newTags[osmkey] = replace.wikidata;
        } else {
          delete newTags[osmkey];
        }
      }
      if (replace && replace.wikipedia !== undefined) {  // replace or delete `*:wikipedia` tag
        changed = true;
        const wpkey = `${prefix}wikipedia`;
        if (replace.wikipedia) {
          newTags[wpkey] = replace.wikipedia;
        } else {
          delete newTags[wpkey];
        }
      }
    }
  });

  // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184
  const isRouteMaster = (tags.type === 'route_master');

  // Gather key/value tag pairs to try to match
  const tryKVs = gatherKVs(tags);
  if (!tryKVs.primary.size && !tryKVs.alternate.size) {
    return changed ? { newTags: newTags, matched: null } : null;
  }

  // Gather namelike tag values to try to match
  const tryNames = gatherNames(tags);

  // Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416
  // If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.
  const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);
  if (foundQID) tryNames.primary.add(foundQID);  // matcher will recognize the Wikidata QID as name too

  if (!tryNames.primary.size && !tryNames.alternate.size) {
    return changed ? { newTags: newTags, matched: null } : null;
  }

  // Order the [key,value,name] tuples - test primary before alternate
  const tuples = gatherTuples(tryKVs, tryNames);

  for (let i = 0; i < tuples.length; i++) {
    const tuple = tuples[i];
    const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc);   // Attempt to match an item in NSI

    if (!hits || !hits.length) continue;  // no match, try next tuple
    if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break;  // a generic match, stop looking

    // A match may contain multiple results, the first one is likely the best one for this location
    // e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`
    let itemID, item;
    for (let j = 0; j < hits.length; j++) {
      const hit = hits[j];
      itemID = hit.itemID;
      if (_nsi.dissolved[itemID]) continue;       // Don't upgrade to a dissolved item

      item = _nsi.ids.get(itemID);
      if (!item) continue;
      const mainTag = item.mainTag;               // e.g. `brand:wikidata`
      const itemQID = item.tags[mainTag];         // e.g. `brand:wikidata` qid
      const notQID = newTags[`not:${mainTag}`];   // e.g. `not:brand:wikidata` qid

      if (                                        // Exceptions, skip this hit
        (!itemQID || itemQID === notQID) ||       // No `*:wikidata` or matched a `not:*:wikidata`
        (newTags.office && !item.tags.office)     // feature may be a corporate office for a brand? - #6416
      ) {
        item = null;
        continue;  // continue looking
      } else {
        break;     // use `item`
      }
    }

    // Can't use any of these hits, try next tuple..
    if (!item) continue;

    // At this point we have matched a canonical item and can suggest tag upgrades..
    item = JSON.parse(JSON.stringify(item));   // deep copy
    const tkv = item.tkv;
    const parts = tkv.split('/', 3);     // tkv = "tree/key/value"
    const k = parts[1];
    const v = parts[2];
    const category = _nsi.data[tkv];
    const properties = category.properties || {};

    // Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)
    let preserveTags = item.preserveTags || properties.preserveTags || [];

    // These tags can be toplevel tags -or- attributes - so we generally want to preserve existing values - #8615
    // We'll only _replace_ the tag value if this tag is the toplevel/defining tag for the matched item (`k`)
    ['building', 'emergency', 'internet_access', 'takeaway'].forEach(osmkey => {
      if (k !== osmkey) preserveTags.push(`^${osmkey}$`);
    });

    const regexes = preserveTags.map(s => new RegExp(s, 'i'));

    let keepTags = {};
    Object.keys(newTags).forEach(osmkey => {
      if (regexes.some(regex => regex.test(osmkey))) {
        keepTags[osmkey] = newTags[osmkey];
      }
    });

    // Remove any primary tags ("amenity", "craft", "shop", "man_made", "route", etc) that have a
    // value like `amenity=yes` or `shop=yes` (exceptions have already been added to `keepTags` above)
    _nsi.kvt.forEach((vmap, k) => {
      if (newTags[k] === 'yes') delete newTags[k];
    });

    // Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`
    if (foundQID) {
      delete newTags.wikipedia;
      delete newTags.wikidata;
    }

    // Do the tag upgrade
    Object.assign(newTags, item.tags, keepTags);

    // Swap `route` back to `route_master` - name-suggestion-index#5184
    if (isRouteMaster) {
      newTags.route_master = newTags.route;
      delete newTags.route;
    }

    // Special `branch` splitting rules - IF..
    // - NSI is suggesting to replace `name`, AND
    // - `branch` doesn't already contain something, AND
    // - original name has not moved to an alternate name (e.g. "Dunkin' Donuts" -> "Dunkin'"), AND
    // - original name is "some name" + "some stuff", THEN
    // consider splitting `name` into `name`/`branch`..
    const origName = tags.name;
    const newName = newTags.name;
    if (newName && origName && newName !== origName && !newTags.branch) {
      const newNames = gatherNames(newTags);
      const newSet = new Set([...newNames.primary, ...newNames.alternate]);
      const isMoved = newSet.has(origName);   // another tag holds the original name now

      if (!isMoved) {
        // Test name fragments, longest to shortest, to fit them into a "Name Branch" pattern.
        // e.g. "TUI ReiseCenter - Neuss Innenstadt" -> ["TUI", "ReiseCenter", "Neuss", "Innenstadt"]
        const nameParts = origName.split(/[\s\-\/,.]/);
        for (let split = nameParts.length; split > 0; split--) {
          const name = nameParts.slice(0, split).join(' ');  // e.g. "TUI ReiseCenter"
          const branch = nameParts.slice(split).join(' ');   // e.g. "Neuss Innenstadt"
          const nameHits = _nsi.matcher.match(k, v, name, loc);
          if (!nameHits || !nameHits.length) continue;    // no match, try next name fragment

          if (nameHits.some(hit => hit.itemID === itemID)) {   // matched the name fragment to the same itemID above
            if (branch) {
              if (notBranches.test(branch)) {   // "branch" was detected but is noise ("factory outlet", etc)
                newTags.name = origName;        // Leave `name` alone, this part of the name may be significant..
              } else {
                const branchHits = _nsi.matcher.match(k, v, branch, loc);
                if (branchHits && branchHits.length) {                                             // if "branch" matched something else in NSI..
                  if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') {  // if another brand! (e.g. "KFC - Taco Bell"?)
                    return null;                                                                   //   bail out - can't suggest tags in this case
                  }                                                                                // else a generic (e.g. "gas", "cafe") - ignore
                } else {                     // "branch" is not noise and not something in NSI
                  newTags.branch = branch;   // Stick it in the `branch` tag..
                }
              }
            }
            break;
          }
        }
      }
    }

    return { newTags: newTags, matched: item };
  }

  return changed ? { newTags: newTags, matched: null } : null;
}