import { normalizeCountryKey, SUPPORTED_COUNTRY_KEYS, } from "./location-support.js"; const supportedCountryKeySet = new Set(SUPPORTED_COUNTRY_KEYS); const LOCATION_ALIASES: Record = { uk: "united kingdom", us: "united states", usa: "united states", }; export function normalizeLocationToken( value: string | null | undefined, ): string { const normalized = value?.trim().toLowerCase().replace(/\s+/g, " ") ?? ""; if (!normalized) return ""; return LOCATION_ALIASES[normalized] ?? normalized; } /** * If search geography includes a supported country token (e.g. "UK", "Canada"), * returns its normalized country key; otherwise null (e.g. "London" only). */ export function inferCountryKeyFromSearchGeography( searchCities?: string | null, jobspyLocation?: string | null, ): string | null { const raw = searchCities?.trim() || jobspyLocation?.trim(); if (!raw) return null; for (const token of parseSearchCitiesSetting(raw)) { const key = normalizeCountryKey(token); if (supportedCountryKeySet.has(key)) return key; } return null; } /** * Parses a job listing location string and returns normalized country keys * (e.g. "London, UK" → ["united kingdom"]). Empty when no supported country tokens. */ export function inferCountryKeysFromJobLocation( location: string | null | undefined, ): string[] { if (!location?.trim()) return []; const keys = new Set(); for (const segment of location.split(/[,;|]/)) { const trimmed = segment.trim(); if (!trimmed) continue; const key = normalizeCountryKey(trimmed); if (supportedCountryKeySet.has(key)) keys.add(key); } const whole = normalizeCountryKey(location); if (supportedCountryKeySet.has(whole)) keys.add(whole); return [...keys]; } export function parseSearchCitiesSetting( value: string | null | undefined, ): string[] { const trimmed = value?.trim(); if (!trimmed) return []; const split = trimmed.includes("|") ? trimmed.split("|") : trimmed.includes("\n") ? trimmed.split("\n") : [trimmed]; const seen = new Set(); const out: string[] = []; for (const raw of split) { const normalized = raw.trim(); if (!normalized) continue; const key = normalized.toLowerCase(); if (seen.has(key)) continue; seen.add(key); out.push(normalized); } return out; } interface ResolveSearchCitiesOptions { list?: string[] | null; single?: string | null; env?: string | null; fallback?: string | null; } export function resolveSearchCities( options: ResolveSearchCitiesOptions, ): string[] { // Priority order: // 1) explicit list (searchCities array in config) // 2) explicit single value // 3) environment fallback // 4) final hardcoded/default fallback if (options.list && options.list.length > 0) { const parsedList = parseSearchCitiesSetting(options.list.join("|")); if (parsedList.length > 0) return parsedList; } const fallbackCandidates = [options.single, options.env, options.fallback]; for (const candidate of fallbackCandidates) { if (candidate === null || candidate === undefined) continue; const parsed = parseSearchCitiesSetting(candidate); if (parsed.length > 0) return parsed; } return []; } export function serializeSearchCitiesSetting(cities: string[]): string | null { if (cities.length === 0) return null; return cities.join("|"); } export function shouldApplyStrictCityFilter( city: string, country: string, ): boolean { const normalizedCity = normalizeLocationToken(city); const normalizedCountry = normalizeCountryKey(country); if (!normalizedCity || !normalizedCountry) return false; return normalizedCity !== normalizedCountry; } export function matchesRequestedCity( jobLocation: string | undefined, requestedCity: string, ): boolean { const normalizedJobLocation = normalizeLocationToken(jobLocation) .replace(/[^a-z0-9]+/g, " ") .trim(); const normalizedRequestedLocation = normalizeLocationToken(requestedCity) .replace(/[^a-z0-9]+/g, " ") .trim(); if (!normalizedJobLocation || !normalizedRequestedLocation) return false; const jobTokens = normalizedJobLocation.split(" "); const requestedTokens = normalizedRequestedLocation.split(" "); if (requestedTokens.length > jobTokens.length) return false; for (let i = 0; i <= jobTokens.length - requestedTokens.length; i += 1) { let matches = true; for (let j = 0; j < requestedTokens.length; j += 1) { if (jobTokens[i + j] !== requestedTokens[j]) { matches = false; break; } } if (matches) return true; } return false; }