mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(salary): preserve Unicode company identity in lookups (#570)
* fix(salary): preserve Unicode company identity in lookups * fix(salary): restore accent folding after Nordic spellings Address MadsLorentzen review feedback on #570: keep Unicode identity normalization and restore ASCII-typed accented-name controls on the documented CLI path.
This commit is contained in:
1 parent
5a29fdc711
commit
894e3917ba
3 files changed
+82
-7
No files matched your search
@@ -463,6 +463,8 @@ per-file diff commands.
|
|||||||
`python3 tools/job_key.py --audit` reports those entries under
|
`python3 tools/job_key.py --audit` reports those entries under
|
||||||
`keys_not_matching_current_rule`; it never rewrites them.
|
`keys_not_matching_current_rule`; it never rewrites them.
|
||||||
|
|
||||||
|
- **Salary lookups preserve Unicode company identities and match ASCII-typed accented names such as `Nestle` to `Nestlé`, while retaining Nordic spelling variants** (#570).
|
||||||
|
|
||||||
## [1.7.2] - 2026-09-29
|
## [1.7.2] - 2026-09-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+18
-7
@@ -174,27 +174,38 @@ def load_data():
|
|||||||
|
|
||||||
def normalize(s):
|
def normalize(s):
|
||||||
"""Normalize string for robust fuzzy matching."""
|
"""Normalize string for robust fuzzy matching."""
|
||||||
s = s.lower().strip()
|
s = unicodedata.normalize("NFC", s.casefold()).strip()
|
||||||
for pat in STRIP_PATTERNS:
|
for pat in STRIP_PATTERNS:
|
||||||
s = re.sub(pat, "", s)
|
s = re.sub(pat, "", s)
|
||||||
s = re.sub(r"[^a-zæøåöäü0-9]", "", s)
|
return "".join(
|
||||||
return s.strip()
|
char for char in s
|
||||||
|
if char.isalnum() or unicodedata.category(char).startswith("M")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def anglicize(s):
|
def anglicize(s):
|
||||||
"""Convert Danish/Nordic characters to anglicized equivalents."""
|
"""Fold Nordic spellings first, then accents for ASCII-typed queries."""
|
||||||
s = s.lower()
|
s = s.lower()
|
||||||
for danish, english in SPELLING_VARIANTS.items():
|
for danish, english in SPELLING_VARIANTS.items():
|
||||||
s = s.replace(danish, english)
|
s = s.replace(danish, english)
|
||||||
return s
|
return "".join(
|
||||||
|
char for char in unicodedata.normalize("NFKD", s)
|
||||||
|
if not unicodedata.combining(char)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def extract_core_words(s):
|
def extract_core_words(s):
|
||||||
"""Extract meaningful words from a company name, ignoring noise."""
|
"""Extract meaningful words from a company name, ignoring noise."""
|
||||||
s = s.lower()
|
s = unicodedata.normalize("NFC", s.casefold())
|
||||||
for pat in STRIP_PATTERNS:
|
for pat in STRIP_PATTERNS:
|
||||||
s = re.sub(pat, "", s)
|
s = re.sub(pat, "", s)
|
||||||
words = re.findall(r"[a-zæøåöäü0-9]+", s)
|
# Keep the same identity as normalize(), including combining marks in
|
||||||
|
# scripts where NFC cannot compose them into a single letter.
|
||||||
|
s = "".join(
|
||||||
|
char if char.isalnum() or unicodedata.category(char).startswith("M") else " "
|
||||||
|
for char in s
|
||||||
|
)
|
||||||
|
words = s.split()
|
||||||
return [w for w in words if len(w) > 1]
|
return [w for w in words if len(w) > 1]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Company identity survives the documented salary-data-file -> CLI path."""
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
|
TOOL = Path(__file__).resolve().parent.parent / "salary_lookup.py"
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeSalaryCLI(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
directory = TemporaryDirectory()
|
||||||
|
self.addCleanup(directory.cleanup)
|
||||||
|
self.root = Path(directory.name)
|
||||||
|
shutil.copyfile(TOOL, self.root / TOOL.name)
|
||||||
|
# Same layout as README_SALARY_TOOL.md's manual-data setup. Keep the
|
||||||
|
# fixture next to an isolated copy of the CLI, never a user's dataset.
|
||||||
|
names = [
|
||||||
|
"腾讯", "東京 AI", "北京 AI", "Acme Corp", "Café Étoile",
|
||||||
|
"Nestlé Danmark A/S", "Ørsted A/S", "Mærsk A/S", "Åland A/S",
|
||||||
|
]
|
||||||
|
data = {"companies": [
|
||||||
|
{"company": name, "categories": {"all_employees": {"index": 105}}}
|
||||||
|
for name in names
|
||||||
|
]}
|
||||||
|
(self.root / "salary_data.json").write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
def lookup(self, query):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(self.root / TOOL.name), query, "--json"],
|
||||||
|
capture_output=True, text=True, encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
|
||||||
|
return [row["company"] for row in json.loads(result.stdout)]
|
||||||
|
|
||||||
|
def test_exact_non_latin_company_can_be_found(self):
|
||||||
|
self.assertEqual(self.lookup("腾讯"), ["腾讯"])
|
||||||
|
|
||||||
|
def test_mixed_script_companies_do_not_collapse_to_a_latin_fragment(self):
|
||||||
|
self.assertEqual(self.lookup("東京 AI")[0], "東京 AI")
|
||||||
|
|
||||||
|
def test_canonically_equivalent_spelling_finds_the_same_company(self):
|
||||||
|
self.assertEqual(self.lookup("Cafe\u0301 E\u0301toile"), ["Café Étoile"])
|
||||||
|
|
||||||
|
def test_ascii_control_still_matches(self):
|
||||||
|
self.assertEqual(self.lookup("acme"), ["Acme Corp"])
|
||||||
|
|
||||||
|
def test_ascii_query_finds_accented_company(self):
|
||||||
|
self.assertEqual(self.lookup("Nestle"), ["Nestlé Danmark A/S"])
|
||||||
|
|
||||||
|
def test_nordic_spelling_variants_survive_accent_folding(self):
|
||||||
|
for query, expected in [("Orsted", "Ørsted A/S"),
|
||||||
|
("Maersk", "Mærsk A/S"),
|
||||||
|
("Aaland", "Åland A/S")]:
|
||||||
|
with self.subTest(query=query):
|
||||||
|
self.assertEqual(self.lookup(query), [expected])
|
||||||
Reference in new issue
Block a user