forked from MadsLorentzen/ai-job-search
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsalary_lookup.py
More file actions
280 lines (230 loc) · 9.21 KB
/
Copy pathsalary_lookup.py
File metadata and controls
280 lines (230 loc) · 9.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#!/usr/bin/env python3
"""
Salary Benchmark Lookup Tool
Looks up company salary data from a user-provided dataset.
Supports any salary data source — union statistics, Glassdoor exports,
manually collected benchmarks, etc.
This tool requires a data file (salary_data.json) that you create
from your own salary data. See tools/README_SALARY_TOOL.md for
instructions on the expected format and how to convert from Excel.
Usage:
python salary_lookup.py "Company Name"
python salary_lookup.py "Company Name" --city "København"
python salary_lookup.py "Company Name" --json
python salary_lookup.py --list-all
"""
import json
import sys
import re
import argparse
import unicodedata
from pathlib import Path
DATA_FILE = Path(__file__).parent / "salary_data.json"
# Common Danish <-> anglicized spelling variants
SPELLING_VARIANTS = {
"ø": "o", "æ": "ae", "å": "aa",
"ö": "o", "ä": "ae", "ü": "u",
}
# Legal suffixes and noise to strip when matching company names
STRIP_PATTERNS = [
r"\ba/s\b", r"\baps\b", r"\bi/s\b", r"\bp/s\b", r"\bk/s\b",
r"\bivs\b", r"\bamba\b", r"\ba\.m\.b\.a\.\b",
r"\(vg\)", r"\(.*?\)", # (VG) and other parentheticals
r"\bdanmark\b", r"\bdenmark\b", r"\bscandinavia\b", r"\bnordic\b",
r"\bgroup\b", r"\bholding\b",
r",\s*.*$", # everything after comma (sub-entities)
]
def load_data():
if not DATA_FILE.exists():
print("Error: salary_data.json not found.", file=sys.stderr)
print("", file=sys.stderr)
print("This tool requires a salary data file.", file=sys.stderr)
print("See tools/README_SALARY_TOOL.md for setup instructions.", file=sys.stderr)
print("", file=sys.stderr)
print("If you don't have salary data, the salary lookup", file=sys.stderr)
print("step will be skipped during /apply.", file=sys.stderr)
sys.exit(1)
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def normalize(s):
"""Normalize string for robust fuzzy matching."""
s = s.lower().strip()
for pat in STRIP_PATTERNS:
s = re.sub(pat, "", s)
s = re.sub(r"[^a-zæøåöäü0-9]", "", s)
return s.strip()
def anglicize(s):
"""Convert Danish/Nordic characters to anglicized equivalents."""
s = s.lower()
for danish, english in SPELLING_VARIANTS.items():
s = s.replace(danish, english)
return s
def extract_core_words(s):
"""Extract meaningful words from a company name, ignoring noise."""
s = s.lower()
for pat in STRIP_PATTERNS:
s = re.sub(pat, "", s)
words = re.findall(r"[a-zæøåöäü0-9]+", s)
return [w for w in words if len(w) > 1]
def match_score(query, entry_name):
"""Compute a match score between 0 and 100 for ranking results."""
q_norm = normalize(query)
n_norm = normalize(entry_name)
if not q_norm or not n_norm:
return 0
if q_norm == n_norm:
return 100
if q_norm in n_norm:
ratio = len(q_norm) / len(n_norm)
if len(q_norm) <= 4 and ratio < 0.5:
q_words = set(extract_core_words(query))
n_words = set(extract_core_words(entry_name))
if not q_words & n_words:
pass
else:
return 80 + int(ratio * 10)
else:
return 80 + int(ratio * 10)
if n_norm in q_norm:
ratio = len(n_norm) / len(q_norm)
if len(n_norm) <= 4 and ratio < 0.5:
pass
else:
return 80 + int(ratio * 10)
q_ang = anglicize(q_norm)
n_ang = anglicize(n_norm)
if q_ang == n_ang:
return 85
if q_ang in n_ang or n_ang in q_ang:
shorter = min(len(q_ang), len(n_ang))
longer = max(len(q_ang), len(n_ang))
if shorter <= 4 and shorter / longer < 0.5:
q_words_ang = {anglicize(w) for w in extract_core_words(query)}
n_words_ang = {anglicize(w) for w in extract_core_words(entry_name)}
if q_words_ang & n_words_ang:
return 75
else:
return 75
q_words = set(extract_core_words(query))
n_words = set(extract_core_words(entry_name))
if not q_words or not n_words:
return 0
overlap = q_words & n_words
if not overlap:
q_words_ang = {anglicize(w) for w in q_words}
n_words_ang = {anglicize(w) for w in n_words}
overlap = q_words_ang & n_words_ang
if overlap:
if len(q_words) == 1:
q_word = list(q_words)[0]
if q_word in n_words or anglicize(q_word) in {anglicize(w) for w in n_words}:
return 70
else:
return 0
coverage = len(overlap) / len(q_words)
return int(30 + coverage * 40)
return 0
def search_company(data, query, city=None):
"""Search for a company by name. Returns matching entries sorted by relevance."""
companies = data.get("companies", [])
scored = []
for entry in companies:
if city:
city_lower = city.lower()
entry_city = entry.get("city", "").lower()
if city_lower not in entry_city and anglicize(city_lower) not in anglicize(entry_city):
continue
score = match_score(query, entry["company"])
if score > 0:
scored.append((score, entry))
scored.sort(key=lambda x: (-x[0], x[1]["company"]))
min_score = 30
return [entry for score, entry in scored if score >= min_score]
def format_entry(entry, metadata):
"""Format a single company entry for display."""
lines = []
lines.append(f"\n{'='*60}")
lines.append(f" {entry['company']}")
if entry.get("city"):
lines.append(f" Location: {entry['city']}")
lines.append(f"{'='*60}")
# Get category data (everything except company/city fields)
categories = entry.get("categories", {})
if not categories:
# Fallback: treat any numeric fields as categories
skip_keys = {"company", "city", "categories"}
for key, value in entry.items():
if key not in skip_keys and isinstance(value, dict):
categories[key] = value
if categories:
index_label = metadata.get("index_label", "Index")
baseline = metadata.get("index_baseline", 100)
lines.append(f" {'Category':<22} {'Count':>6} {index_label:>8} {'vs Baseline':>10}")
lines.append(f" {'-'*50}")
for label, data in categories.items():
display_label = label.replace("_", " ").title()
count = data.get("count")
index = data.get("index")
if count is not None or index is not None:
count_str = str(count) if count is not None else "-"
if isinstance(index, (int, float)):
diff = index - baseline
sign = "+" if diff >= 0 else ""
index_str = f"{index:.1f}"
diff_str = f"{sign}{diff:.1f}%"
elif index is not None:
index_str = str(index)
diff_str = ""
else:
index_str = "N/A*"
diff_str = ""
lines.append(f" {display_label:<22} {count_str:>6} {index_str:>8} {diff_str:>10}")
lines.append(f"\n * N/A = Too few employees to publish (privacy)")
if metadata.get("baseline_description"):
lines.append(f" {metadata['baseline_description']}")
else:
lines.append(f" {index_label} {baseline} = baseline")
else:
# Simple format: just show all non-standard fields
skip_keys = {"company", "city", "categories"}
for key, value in entry.items():
if key not in skip_keys:
display_key = key.replace("_", " ").title()
lines.append(f" {display_key}: {value}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Salary Benchmark Lookup")
parser.add_argument("company", nargs="?", help="Company name to search for")
parser.add_argument("--city", help="Filter by city name")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--list-all", action="store_true", help="List all companies")
args = parser.parse_args()
data = load_data()
metadata = data.get("metadata", {})
companies = data.get("companies", [])
if args.list_all:
for entry in companies:
city = entry.get("city", "")
city_str = f" ({city})" if city else ""
print(f"{entry['company']}{city_str}")
return
if not args.company:
parser.print_help()
sys.exit(1)
results = search_company(data, args.company, args.city)
if not results:
print(f"No results found for '{args.company}'")
if args.city:
print(f" (filtered by city: {args.city})")
print("\nTry a shorter or different name. Company names in the dataset")
print("may include legal suffixes like 'A/S' or 'ApS'.")
sys.exit(1)
if args.json:
print(json.dumps(results, ensure_ascii=False, indent=2))
else:
print(f"\nFound {len(results)} match(es) for '{args.company}':")
for entry in results:
print(format_entry(entry, metadata))
print()
if __name__ == "__main__":
main()