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 Handling of arbitrary postcode tokens in tokenized query string.
 
  10 from typing import Tuple, Set
 
  12 from collections import defaultdict
 
  16 from ..config import Configuration
 
  17 from . import query as qmod
 
  21     """ Pattern-based parser for postcodes in tokenized queries.
 
  23         The postcode patterns are read from the country configuration.
 
  24         The parser does currently not return country restrictions.
 
  27     def __init__(self, config: Configuration) -> None:
 
  28         # skip over includes here to avoid loading the complete country name data
 
  29         yaml.add_constructor('!include', lambda loader, node: [],
 
  30                              Loader=yaml.SafeLoader)
 
  31         cdata = yaml.safe_load(config.find_config_file('country_settings.yaml')
 
  32                                      .read_text(encoding='utf-8'))
 
  34         unique_patterns = defaultdict(set)
 
  35         for cc, data in cdata.items():
 
  36             if data.get('postcode'):
 
  37                 pat = data['postcode']['pattern']
 
  38                 out = data['postcode'].get('output')
 
  39                 unique_patterns[pat.replace('d', '[0-9]').replace('l', '[a-z]')].add(out)
 
  41         self.global_pattern = re.compile(
 
  43                 '|'.join(f"(?:{k})" for k in unique_patterns)
 
  46         self.local_patterns = [(re.compile(f"(?:{k})[:, >]"), v)
 
  47                                for k, v in unique_patterns.items()]
 
  49     def parse(self, query: qmod.QueryStruct) -> Set[Tuple[int, int, str]]:
 
  50         """ Parse postcodes in the given list of query tokens taking into
 
  51             account the list of breaks from the nodes.
 
  53             The result is a sequence of tuples with
 
  54             [start node id, end node id, postcode token]
 
  59         for i in range(query.num_token_slots()):
 
  60             if nodes[i].btype in '<,: ' and nodes[i + 1].btype != '`':
 
  61                 word = nodes[i + 1].term_normalized + nodes[i + 1].btype
 
  62                 if word[-1] in ' -' and nodes[i + 2].btype != '`':
 
  63                     word += nodes[i + 2].term_normalized + nodes[i + 2].btype
 
  64                     if word[-1] in ' -' and nodes[i + 3].btype != '`':
 
  65                         word += nodes[i + 3].term_normalized + nodes[i + 3].btype
 
  67                 # Use global pattern to check for presence of any postocde.
 
  68                 m = self.global_pattern.match(word)
 
  70                     # If there was a match, check against each pattern separately
 
  71                     # because multiple patterns might be machting at the end.
 
  72                     for pattern, info in self.local_patterns:
 
  73                         lm = pattern.match(word)
 
  75                             trange = (i, i + sum(c in ' ,-:>' for c in lm.group(0)))
 
  78                                     outcodes.add((*trange, lm.expand(out).upper()))
 
  80                                     outcodes.add((*trange, lm.group(0)[:-1].upper()))