]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tokenizer/token_analysis/config_variants.py
Merge pull request #2731 from lonvia/cleanup-special-phrases
[nominatim.git] / nominatim / tokenizer / token_analysis / config_variants.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Parser for configuration for variants.
9 """
10 from collections import defaultdict, namedtuple
11 import itertools
12 import re
13
14 from icu import Transliterator
15
16 from nominatim.config import flatten_config_list
17 from nominatim.errors import UsageError
18
19 ICUVariant = namedtuple('ICUVariant', ['source', 'replacement'])
20
21 def get_variant_config(rules, normalization_rules):
22     """ Convert the variant definition from the configuration into
23         replacement sets.
24
25         Returns a tuple containing the replacement set and the list of characters
26         used in the replacements.
27     """
28     immediate = defaultdict(list)
29     chars = set()
30
31     if rules:
32         vset = set()
33         rules = flatten_config_list(rules, 'variants')
34
35         vmaker = _VariantMaker(normalization_rules)
36
37         for section in rules:
38             for rule in (section.get('words') or []):
39                 vset.update(vmaker.compute(rule))
40
41         # Intermediate reorder by source. Also compute required character set.
42         for variant in vset:
43             if variant.source[-1] == ' ' and variant.replacement[-1] == ' ':
44                 replstr = variant.replacement[:-1]
45             else:
46                 replstr = variant.replacement
47             immediate[variant.source].append(replstr)
48             chars.update(variant.source)
49
50     return list(immediate.items()), ''.join(chars)
51
52
53 class _VariantMaker:
54     """ Generater for all necessary ICUVariants from a single variant rule.
55
56         All text in rules is normalized to make sure the variants match later.
57     """
58
59     def __init__(self, norm_rules):
60         self.norm = Transliterator.createFromRules("rule_loader_normalization",
61                                                    norm_rules)
62
63
64     def compute(self, rule):
65         """ Generator for all ICUVariant tuples from a single variant rule.
66         """
67         parts = re.split(r'(\|)?([=-])>', rule)
68         if len(parts) != 4:
69             raise UsageError(f"Syntax error in variant rule: {rule}")
70
71         decompose = parts[1] is None
72         src_terms = [self._parse_variant_word(t) for t in parts[0].split(',')]
73         repl_terms = (self.norm.transliterate(t).strip() for t in parts[3].split(','))
74
75         # If the source should be kept, add a 1:1 replacement
76         if parts[2] == '-':
77             for src in src_terms:
78                 if src:
79                     for froms, tos in _create_variants(*src, src[0], decompose):
80                         yield ICUVariant(froms, tos)
81
82         for src, repl in itertools.product(src_terms, repl_terms):
83             if src and repl:
84                 for froms, tos in _create_variants(*src, repl, decompose):
85                     yield ICUVariant(froms, tos)
86
87
88     def _parse_variant_word(self, name):
89         name = name.strip()
90         match = re.fullmatch(r'([~^]?)([^~$^]*)([~$]?)', name)
91         if match is None or (match.group(1) == '~' and match.group(3) == '~'):
92             raise UsageError(f"Invalid variant word descriptor '{name}'")
93         norm_name = self.norm.transliterate(match.group(2)).strip()
94         if not norm_name:
95             return None
96
97         return norm_name, match.group(1), match.group(3)
98
99
100 _FLAG_MATCH = {'^': '^ ',
101                '$': ' ^',
102                '': ' '}
103
104
105 def _create_variants(src, preflag, postflag, repl, decompose):
106     if preflag == '~':
107         postfix = _FLAG_MATCH[postflag]
108         # suffix decomposition
109         src = src + postfix
110         repl = repl + postfix
111
112         yield src, repl
113         yield ' ' + src, ' ' + repl
114
115         if decompose:
116             yield src, ' ' + repl
117             yield ' ' + src, repl
118     elif postflag == '~':
119         # prefix decomposition
120         prefix = _FLAG_MATCH[preflag]
121         src = prefix + src
122         repl = prefix + repl
123
124         yield src, repl
125         yield src + ' ', repl + ' '
126
127         if decompose:
128             yield src, repl + ' '
129             yield src + ' ', repl
130     else:
131         prefix = _FLAG_MATCH[preflag]
132         postfix = _FLAG_MATCH[postflag]
133
134         yield prefix + src + postfix, prefix + repl + postfix