def _match_by_norm_func(l1, l2, norm_fn, dist_fn, thresh):
    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