2026-03-20 19:53:16 +01:00
|
|
|
#!/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",
|
2026-08-23 12:56:21 +05:30
|
|
|
r"\bivs\b", r"\bamba\b", r"\ba\.m\.b\.a\.?\b",
|
2026-03-20 19:53:16 +01:00
|
|
|
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)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-07-13 14:55:55 +01:00
|
|
|
def fail_data_error(message):
|
|
|
|
|
"""Exit with a user-facing salary data setup error."""
|
|
|
|
|
print(f"Error: invalid salary_data.json: {message}", file=sys.stderr)
|
|
|
|
|
print("", file=sys.stderr)
|
|
|
|
|
print("See tools/README_SALARY_TOOL.md for the expected format.", file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
def collect_validation_issues(data):
|
|
|
|
|
"""Return (errors, warnings) for the salary data shape.
|
|
|
|
|
|
|
|
|
|
errors -> hard problems that make lookups crash or emit wrong output
|
|
|
|
|
(these cause validate_data() to exit(1)).
|
|
|
|
|
warnings -> usability concerns that still work (e.g. duplicate company
|
|
|
|
|
names); --validate reports them but exits 0.
|
|
|
|
|
"""
|
|
|
|
|
errors = []
|
|
|
|
|
warnings = []
|
|
|
|
|
|
2026-07-13 14:55:55 +01:00
|
|
|
if not isinstance(data, dict):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append("top-level JSON value must be an object")
|
|
|
|
|
return errors, warnings
|
2026-07-13 14:55:55 +01:00
|
|
|
|
|
|
|
|
metadata = data.get("metadata", {})
|
|
|
|
|
if metadata is not None and not isinstance(metadata, dict):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append("'metadata' must be an object when provided")
|
2026-07-13 14:55:55 +01:00
|
|
|
|
|
|
|
|
companies = data.get("companies")
|
|
|
|
|
if not isinstance(companies, list):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append("'companies' must be a list")
|
|
|
|
|
return errors, warnings
|
2026-07-13 14:55:55 +01:00
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
seen_companies = {}
|
2026-07-13 14:55:55 +01:00
|
|
|
for index, entry in enumerate(companies, start=1):
|
|
|
|
|
if not isinstance(entry, dict):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append(f"companies[{index}] must be an object")
|
|
|
|
|
continue
|
2026-07-13 14:55:55 +01:00
|
|
|
|
|
|
|
|
company = entry.get("company")
|
|
|
|
|
if not isinstance(company, str) or not company.strip():
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append(f"companies[{index}].company must be a non-empty string")
|
|
|
|
|
else:
|
|
|
|
|
key = company.lower()
|
|
|
|
|
if key in seen_companies:
|
|
|
|
|
warnings.append(
|
|
|
|
|
f"Duplicate company name '{company}' "
|
|
|
|
|
f"(companies[{seen_companies[key]}] and companies[{index}])"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
seen_companies[key] = index
|
2026-07-13 14:55:55 +01:00
|
|
|
|
|
|
|
|
city = entry.get("city")
|
|
|
|
|
if city is not None and not isinstance(city, str):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append(f"companies[{index}].city must be a string when provided")
|
2026-07-13 14:55:55 +01:00
|
|
|
|
|
|
|
|
categories = entry.get("categories", {})
|
|
|
|
|
if categories is not None and not isinstance(categories, dict):
|
2026-07-15 06:53:01 +01:00
|
|
|
errors.append(f"companies[{index}].categories must be an object when provided")
|
|
|
|
|
elif categories:
|
|
|
|
|
for cat_label, cat_data in categories.items():
|
|
|
|
|
if not isinstance(cat_data, dict):
|
|
|
|
|
errors.append(
|
|
|
|
|
f"companies[{index}].categories.{cat_label} must be an object "
|
|
|
|
|
f"with 'count' and/or 'index' (got {type(cat_data).__name__})"
|
|
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
count = cat_data.get("count")
|
|
|
|
|
if count is not None and not isinstance(count, (int, float)):
|
|
|
|
|
errors.append(
|
|
|
|
|
f"companies[{index}].categories.{cat_label}.count must be a "
|
|
|
|
|
f"number (got {type(count).__name__})"
|
|
|
|
|
)
|
|
|
|
|
index_val = cat_data.get("index")
|
|
|
|
|
if index_val is not None and not isinstance(index_val, (int, float, str)):
|
|
|
|
|
errors.append(
|
|
|
|
|
f"companies[{index}].categories.{cat_label}.index must be a "
|
|
|
|
|
f"number or string (got {type(index_val).__name__})"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return errors, warnings
|
2026-07-13 14:55:55 +01:00
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
|
|
|
|
|
def validate_data(data):
|
|
|
|
|
"""Validate the salary data shape before lookups use it.
|
|
|
|
|
|
|
|
|
|
Preserves historical behavior: exits(1) on the first hard error with the
|
|
|
|
|
same user-facing message, and returns data unchanged when valid.
|
|
|
|
|
"""
|
|
|
|
|
errors, _ = collect_validation_issues(data)
|
|
|
|
|
if errors:
|
|
|
|
|
fail_data_error(errors[0])
|
2026-07-13 14:55:55 +01:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
def read_raw_data():
|
|
|
|
|
"""Load and JSON-parse salary_data.json; exit with a helpful message if missing/invalid."""
|
2026-03-20 19:53:16 +01:00
|
|
|
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)
|
2026-07-13 14:55:55 +01:00
|
|
|
try:
|
|
|
|
|
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
except json.JSONDecodeError as exc:
|
|
|
|
|
fail_data_error(f"invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}")
|
2026-07-15 06:53:01 +01:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_data():
|
|
|
|
|
"""Load, parse, and validate salary_data.json for lookups."""
|
|
|
|
|
return validate_data(read_raw_data())
|
2026-03-20 19:53:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
def match_score_optimized(q_norm, q_ang, q_words_set, q_words_ang_set, query, entry_name):
|
|
|
|
|
"""Compute a match score between 0 and 100 using precalculated query values."""
|
2026-03-20 19:53:16 +01:00
|
|
|
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:
|
|
|
|
|
n_words = set(extract_core_words(entry_name))
|
2026-07-10 14:24:20 +01:00
|
|
|
if not q_words_set & n_words:
|
2026-03-20 19:53:16 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
n_words_ang = {anglicize(w) for w in extract_core_words(entry_name)}
|
2026-07-10 14:24:20 +01:00
|
|
|
if q_words_ang_set & n_words_ang:
|
2026-03-20 19:53:16 +01:00
|
|
|
return 75
|
|
|
|
|
else:
|
|
|
|
|
return 75
|
|
|
|
|
|
|
|
|
|
n_words = set(extract_core_words(entry_name))
|
2026-07-10 14:24:20 +01:00
|
|
|
if not q_words_set or not n_words:
|
2026-03-20 19:53:16 +01:00
|
|
|
return 0
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
overlap = q_words_set & n_words
|
2026-03-20 19:53:16 +01:00
|
|
|
if not overlap:
|
|
|
|
|
n_words_ang = {anglicize(w) for w in n_words}
|
2026-07-10 14:24:20 +01:00
|
|
|
overlap = q_words_ang_set & n_words_ang
|
2026-03-20 19:53:16 +01:00
|
|
|
|
|
|
|
|
if overlap:
|
2026-07-10 14:24:20 +01:00
|
|
|
if len(q_words_set) == 1:
|
|
|
|
|
q_word = list(q_words_set)[0]
|
2026-03-20 19:53:16 +01:00
|
|
|
if q_word in n_words or anglicize(q_word) in {anglicize(w) for w in n_words}:
|
|
|
|
|
return 70
|
|
|
|
|
else:
|
|
|
|
|
return 0
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
coverage = len(overlap) / len(q_words_set)
|
2026-03-20 19:53:16 +01:00
|
|
|
return int(30 + coverage * 40)
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
def match_score(query, entry_name):
|
|
|
|
|
"""Compute a match score between 0 and 100 for ranking results."""
|
|
|
|
|
q_norm = normalize(query)
|
|
|
|
|
q_ang = anglicize(q_norm)
|
|
|
|
|
q_words = extract_core_words(query)
|
|
|
|
|
q_words_set = set(q_words)
|
|
|
|
|
q_words_ang_set = {anglicize(w) for w in q_words}
|
|
|
|
|
return match_score_optimized(q_norm, q_ang, q_words_set, q_words_ang_set, query, entry_name)
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 19:53:16 +01:00
|
|
|
def search_company(data, query, city=None):
|
|
|
|
|
"""Search for a company by name. Returns matching entries sorted by relevance."""
|
|
|
|
|
companies = data.get("companies", [])
|
|
|
|
|
scored = []
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
# Pre-calculate query representations once to avoid redundant computations inside the loop
|
|
|
|
|
q_norm = normalize(query)
|
|
|
|
|
q_ang = anglicize(q_norm)
|
|
|
|
|
q_words = extract_core_words(query)
|
|
|
|
|
q_words_set = set(q_words)
|
|
|
|
|
q_words_ang_set = {anglicize(w) for w in q_words}
|
|
|
|
|
|
2026-03-20 19:53:16 +01:00
|
|
|
for entry in companies:
|
|
|
|
|
if city:
|
|
|
|
|
city_lower = city.lower()
|
2026-07-10 00:44:15 +05:30
|
|
|
entry_city = (entry.get("city") or "").lower()
|
2026-03-20 19:53:16 +01:00
|
|
|
if city_lower not in entry_city and anglicize(city_lower) not in anglicize(entry_city):
|
|
|
|
|
continue
|
|
|
|
|
|
2026-07-10 14:24:20 +01:00
|
|
|
score = match_score_optimized(q_norm, q_ang, q_words_set, q_words_ang_set, query, entry["company"])
|
2026-03-20 19:53:16 +01:00
|
|
|
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."""
|
2026-09-02 01:05:59 +05:30
|
|
|
# `metadata` and `entry["categories"]` may be an explicit null: --validate
|
|
|
|
|
# treats a null the same as an omitted key ("...must be an object when
|
|
|
|
|
# provided"), but dict.get(key, default) only substitutes the default for an
|
|
|
|
|
# absent key, so a null reached `.get()`/`[]` here and crashed the lookup.
|
|
|
|
|
metadata = metadata or {}
|
2026-03-20 19:53:16 +01:00
|
|
|
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}")
|
|
|
|
|
|
2026-09-02 01:05:59 +05:30
|
|
|
# Get category data (everything except company/city fields).
|
|
|
|
|
categories = entry.get("categories") or {}
|
2026-03-20 19:53:16 +01:00
|
|
|
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:
|
2026-07-08 22:12:36 +03:00
|
|
|
count_str = str(count) if count is not None else "-"
|
|
|
|
|
if isinstance(index, (int, float)):
|
2026-03-20 19:53:16 +01:00
|
|
|
index_str = f"{index:.1f}"
|
2026-07-10 00:44:15 +05:30
|
|
|
if baseline == 0:
|
|
|
|
|
diff_str = ""
|
|
|
|
|
else:
|
|
|
|
|
diff_pct = ((index - baseline) / baseline) * 100
|
|
|
|
|
sign = "+" if diff_pct >= 0 else ""
|
|
|
|
|
diff_str = f"{sign}{diff_pct:.1f}%"
|
2026-07-08 22:12:36 +03:00
|
|
|
elif index is not None:
|
|
|
|
|
index_str = str(index)
|
|
|
|
|
diff_str = ""
|
2026-03-20 19:53:16 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
def print_validation_report(errors, warnings):
|
|
|
|
|
"""Print an actionable validation report. Returns the process exit code."""
|
|
|
|
|
if not errors and not warnings:
|
|
|
|
|
print("OK - no issues found.")
|
|
|
|
|
return 0
|
|
|
|
|
print(f"Found {len(errors) + len(warnings)} issue(s):")
|
|
|
|
|
if errors:
|
|
|
|
|
print(" Errors:")
|
|
|
|
|
for i, msg in enumerate(errors, start=1):
|
|
|
|
|
print(f" [{i}] {msg}")
|
|
|
|
|
if warnings:
|
|
|
|
|
print(" Warnings:")
|
|
|
|
|
for i, msg in enumerate(warnings, start=1):
|
|
|
|
|
print(f" [{i}] {msg}")
|
|
|
|
|
if errors:
|
|
|
|
|
print("")
|
|
|
|
|
print("Fix the errors above, then re-run. See tools/README_SALARY_TOOL.md "
|
|
|
|
|
"for the expected format.")
|
|
|
|
|
return 1
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 19:53:16 +01:00
|
|
|
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")
|
2026-07-15 06:53:01 +01:00
|
|
|
parser.add_argument("--validate", action="store_true",
|
|
|
|
|
help="Validate salary_data.json and print a report, then exit")
|
2026-03-20 19:53:16 +01:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
2026-07-15 06:53:01 +01:00
|
|
|
if args.validate:
|
|
|
|
|
data = read_raw_data()
|
|
|
|
|
errors, warnings = collect_validation_issues(data)
|
|
|
|
|
print(f"Validating {DATA_FILE.name} ...")
|
|
|
|
|
print("")
|
|
|
|
|
sys.exit(print_validation_report(errors, warnings))
|
|
|
|
|
|
2026-03-20 19:53:16 +01:00
|
|
|
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()
|