import type { Match, NormalizedJob } from "./types.ts"; const EU_COUNTRIES = [ "france", "germany", "spain", "italy", "portugal", "netherlands", "belgium", "luxembourg", "austria", "switzerland", "ireland", "united kingdom", "sweden", "norway", "denmark", "finland", "poland", "czech", "czechia", "romania", "hungary", "greece", "croatia", "slovenia", "slovakia", "estonia", "latvia", "lithuania", "bulgaria", "europe", "european", "emea", ]; const AGENCY_DENY = [ "lemon.io", "g2i", "distantjob", "talent sam", "kdci", "2am.tech", "photon", "turing", "toptal", "andela", "telus digital", "a.team", "white cloak", "creative chaos", "dacodes", "coalition technologies", "jobs for humanity", "jobsforlebanon", "consulting1x1", ]; const JUNIOR = /\b(intern|internship|trainee|stage\b|apprentice|junior|entry-level|entry level|software engineer i\b)\b/i; const WRONG_ROLE = /\b(qa engineer|quality assurance|copywriter|marketing|sales|account executive|customer support|support engineer|devops|data engineer|scraping engineer|documentation engineer|ux designer|head of engineering|game developer)\b/i; const WRONG_STACK = /\b(reactjs|react\.js|\breact\b|next\.js|angular|ruby|rails|django|\.net|asp\.net|spring ?boot|\bjava\b|\bphp\b|laravel|symfony|kotlin|golang|flutter)\b/i; const VUE = /\bvue(?:\.?js)?\b/i; const NUXT = /\bnuxt(?:\.?js)?\b/i; function tokens(location: string): string[] { const parts = location .toLowerCase() .split(/[,/;|]+/) .map((part) => part.trim()) .filter(Boolean); if (parts.length === 0 && location.trim()) return [location.toLowerCase().trim()]; return parts; } function isEuToken(token: string): boolean { if (token === "uk" || token === "gb" || token === "eu") return true; return EU_COUNTRIES.some((country) => token === country || token.startsWith(`${country} `)); } function geoEligible(job: NormalizedJob): { ok: boolean; reason?: string; bonus: number } { const loc = job.location.toLowerCase().trim(); if (!loc || loc === "worldwide" || loc === "anywhere" || loc === "remote") { return { ok: true, bonus: 1, reason: "remote worldwide" }; } const parts = tokens(job.location); const euParts = parts.filter(isEuToken); if (euParts.length === 0) return { ok: false, bonus: 0 }; const france = parts.some((part) => part === "france" || part.includes("france")); return { ok: true, bonus: france ? 6 : 3, reason: france ? "France" : "Europe", }; } function timezoneOk(job: NormalizedJob): boolean { if (job.timezones.length === 0) return true; return job.timezones.some((tz) => tz >= 0 && tz <= 3); } export function matchJob(job: NormalizedJob): Match | null { const company = job.company.toLowerCase(); if (AGENCY_DENY.some((name) => company.includes(name))) return null; const title = job.title; if (JUNIOR.test(title) || WRONG_ROLE.test(title)) return null; if (/\b(hybrid|présentiel|on-?site|short term|fluent ukrainian)\b/i.test(title)) return null; if (!job.remote) return null; if (!timezoneOk(job)) return null; const geo = geoEligible(job); if (!geo.ok) return null; const vueTitle = VUE.test(title); const nuxtTitle = NUXT.test(title) || /\bsidebase\b/i.test(title); const conflictingTitle = WRONG_STACK.test(title) && !vueTitle && !nuxtTitle; if (conflictingTitle) return null; if (!vueTitle && !nuxtTitle) return null; const body = `${job.excerpt} ${job.description}`.toLowerCase().slice(0, 4000); const reasons: string[] = []; let score = geo.bonus; if (geo.reason) reasons.push(geo.reason); if (nuxtTitle) { score += 8; reasons.push("Nuxt"); } if (vueTitle) { score += 7; reasons.push("Vue"); } if (/\btypescript\b/i.test(`${title} ${job.tags.join(" ")} ${body}`)) { score += 2; reasons.push("TypeScript"); } if (/\bnode(?:\.?js)?\b/i.test(`${title} ${job.tags.join(" ")} ${body}`)) { score += 2; reasons.push("Node.js"); } if (/\bfull[\s-]?stack\b/i.test(title)) { score += 2; reasons.push("Fullstack"); } else if (/\bfront[\s-]?end\b/i.test(title)) { score += 1; reasons.push("Frontend"); } if (/\b(travel|booking|ndc|gds|fintech|b2b)\b/i.test(`${title} ${body}`)) { score += 2; reasons.push("domaine proche"); } if (score < 8) return null; return { job, score, reasons }; }