docs: def _match_by_norm_func(l1, l2, norm_fn, dist_fn, thresh):
      """Matches elements in l1 and l2 using normalization functions.
  
      Splits the elements in each list into buckets given by the normalization
      function. If the same normalization value points to a bucket from the
      first list and a bucket from the second list, both with a single element
      we consider the elements in the list as matching if the distance between
      them is less (or equal) than the threshold.
  
      e.g. l1 = ['X1', 'Y1', 'Y2', 'Z5'], l2 = ['X1', 'Y3', 'Z1']
           norm_fn = lambda x: x[0]
           dist_fn = lambda e1, e2: 0 if e1 == e2 else 1
           thresh = 0
  
      The buckets will then be:
          l1_bucket = {'X': ['X1'], 'Y': ['Y1', 'Y2'], 'Z': ['Z5']}
          l2_bucket = {'X': ['X1'], 'Y': ['Y3'], 'Z': ['Z1']}
  
      For each normalized value:
          'X' -> consider 'X1' equal with 'X1' since the distance is equal with
                 the thershold
          'Y' -> skip the lists since we have multiple possible matches
          'Z' -> consider 'Z1' and 'Z5' as different since the distance is
                 greater than the threshold.
      Return:
          [('X1', 'X2')]
      """
      common = []
  
      l1_only_idx = set(range(len(l1)))
      l2_only_idx = set(range(len(l2)))
  
      buckets_l1 = _group_by_fn(enumerate(l1), lambda x: norm_fn(x[1]))
      buckets_l2 = _group_by_fn(enumerate(l2), lambda x: norm_fn(x[1]))
  
      for normed, l1_elements in buckets_l1.items():
          l2_elements = buckets_l2.get(normed, [])
          if not l1_elements or not l2_elements:
              continue
          _, (_, e1_first) = l1_elements[0]
          _, (_, e2_first) = l2_elements[0]
          match_is_ambiguous = not (
              len(l1_elements) == len(l2_elements) and (
                  all(e2 == e2_first for (_, (_, e2)) in l2_elements) or
                  all(e1 == e1_first for (_, (_, e1)) in l1_elements)
              )
          )
          if match_is_ambiguous:
              continue
          for (e1_idx, e1), (e2_idx, e2) in zip(l1_elements, l2_elements):
              if dist_fn(e1, e2) > thresh:
                  continue
              l1_only_idx.remove(e1_idx)
              l2_only_idx.remove(e2_idx)
              common.append((e1, e2))
  
      l1_only = [l1[i] for i in l1_only_idx]
      l2_only = [l2[i] for i in l2_only_idx]
  
      return common, l1_only, l2_only