1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2025 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Generic processor for names that creates abbreviation variants.
 
  10 from typing import Mapping, Dict, Any, Iterable, Optional, List, cast, Tuple
 
  13 from ...errors import UsageError
 
  14 from ...data.place_name import PlaceName
 
  15 from .config_variants import get_variant_config
 
  16 from .generic_mutation import MutationVariantGenerator
 
  17 from .simple_trie import SimpleTrie
 
  19 # Configuration section
 
  22 def configure(rules: Mapping[str, Any], normalizer: Any, _: Any) -> Dict[str, Any]:
 
  23     """ Extract and preprocess the configuration for this module.
 
  25     config: Dict[str, Any] = {}
 
  27     config['replacements'], _ = get_variant_config(rules.get('variants'), normalizer)
 
  28     config['variant_only'] = rules.get('mode', '') == 'variant-only'
 
  30     # parse mutation rules
 
  31     config['mutations'] = []
 
  32     for rule in rules.get('mutations', []):
 
  33         if 'pattern' not in rule:
 
  34             raise UsageError("Missing field 'pattern' in mutation configuration.")
 
  35         if not isinstance(rule['pattern'], str):
 
  36             raise UsageError("Field 'pattern' in mutation configuration "
 
  37                              "must be a simple text field.")
 
  38         if 'replacements' not in rule:
 
  39             raise UsageError("Missing field 'replacements' in mutation configuration.")
 
  40         if not isinstance(rule['replacements'], list):
 
  41             raise UsageError("Field 'replacements' in mutation configuration "
 
  42                              "must be a list of texts.")
 
  44         config['mutations'].append((rule['pattern'], rule['replacements']))
 
  51 def create(normalizer: Any, transliterator: Any,
 
  52            config: Mapping[str, Any]) -> 'GenericTokenAnalysis':
 
  53     """ Create a new token analysis instance for this module.
 
  55     return GenericTokenAnalysis(normalizer, transliterator, config)
 
  58 class GenericTokenAnalysis:
 
  59     """ Collects the different transformation rules for normalisation of names
 
  60         and provides the functions to apply the transformations.
 
  63     def __init__(self, norm: Any, to_ascii: Any, config: Mapping[str, Any]) -> None:
 
  65         self.to_ascii = to_ascii
 
  66         self.variant_only = config['variant_only']
 
  69         self.replacements: Optional[SimpleTrie[List[str]]] = \
 
  70             SimpleTrie(config['replacements']) if config['replacements'] else None
 
  72         # set up mutation rules
 
  73         self.mutations = [MutationVariantGenerator(*cfg) for cfg in config['mutations']]
 
  75     def get_canonical_id(self, name: PlaceName) -> str:
 
  76         """ Return the normalized form of the name. This is the standard form
 
  77             from which possible variants for the name can be derived.
 
  79         return cast(str, self.norm.transliterate(name.name)).strip()
 
  81     def compute_variants(self, norm_name: str) -> Tuple[List[str], List[str]]:
 
  82         """ Compute the spelling variants for the given normalized name
 
  83             and transliterate the result.
 
  85         variants = self._generate_word_variants(norm_name)
 
  87         for mutation in self.mutations:
 
  88             variants = mutation.generate(variants)
 
  90         varset = set(map(str.strip, variants))
 
  92             varset.discard(norm_name)
 
  98             t = self.to_ascii.transliterate(var).strip()
 
 105     def _generate_word_variants(self, norm_name: str) -> Iterable[str]:
 
 106         baseform = '^ ' + norm_name + ' ^'
 
 107         baselen = len(baseform)
 
 111         if self.replacements is not None:
 
 116                 repl, pos = self.replacements.longest_prefix(baseform, pos)
 
 118                     done = baseform[startpos:frm]
 
 119                     partials = [v + done + r
 
 120                                 for v, r in itertools.product(partials, repl)
 
 121                                 if not force_space or r.startswith(' ')]
 
 122                     if len(partials) > 128:
 
 123                         # If too many variants are produced, they are unlikely
 
 124                         # to be helpful. Only use the original term.
 
 127                     if baseform[pos - 1] == ' ':
 
 135         # No variants detected? Fast return.
 
 139         if startpos < baselen:
 
 140             return (part[1:] + baseform[startpos:-1] for part in partials)
 
 142         return (part[1:-1] for part in partials)