export type WorkType = "onsite" | "remote" | "hybrid"; export type JobType = "full-time" | "part-time" | "contract" | "temporary" | "internship"; export type TimePosted = "day" | "week" | "month"; export type Seniority = "internship" | "entry" | "associate" | "mid-senior" | "director" | "executive"; export interface SearchConfig { label: string; /** Use a raw LinkedIn URL instead of building one from filters. */ url?: string; keywords?: string; geoId?: string; location?: string; workType?: WorkType[]; jobType?: JobType[]; timePosted?: TimePosted; seniority?: Seniority[]; } export const KEYWORDS = [ "Software Engineer", "Solutions Engineer", "Full Stack Engineer", "Backend Engineer", "Data Engineer", "AI Engineer", "Applications Engineer", "Platform Engineer", "DevOps Engineer", "Staff Engineer", "Principal Engineer", "Tech Lead", "Developer", "Ruby on Rails", "Python", "Node.js", "TypeScript", "Artificial Intelligence", "Machine Learning", "Data Science", "AI Consultant", "ML Engineer", "Director of Engineering", "Engineering Manager", "Lead Developer", "System Engineer", "Solution Engineer", ].join(" OR "); // --- Role families beyond the core engineer/developer titles --- // Kept as separate OR-strings rather than folded into KEYWORDS: LinkedIn ranks // a focused query far better than one 50-term disjunction, and each family // needs a different set of areas (research is Basel-heavy, field/remote is not). /** Platform, reliability, cloud and security — the DevOps/Swisscom lane. */ export const PLATFORM_KEYWORDS = [ "Solutions Architect", "Cloud Architect", "Site Reliability Engineer", "Infrastructure Engineer", "Cloud Engineer", "Security Engineer", "DevSecOps", "Application Security Engineer", "Cloud Security Engineer", "MLOps Engineer", "AI Platform Engineer", "ML Platform Engineer", "AI Infrastructure", ].join(" OR "); /** Research software engineering — Basel pharma/university cluster. * Deliberately no bioinformatics/computational-biology terms: those are * domain-science roles, filtered out by BIO_DOMAIN_RE in fetch.ts. */ export const RESEARCH_KEYWORDS = [ "Research Software Engineer", "Scientific Software Engineer", "Scientific Programmer", "Scientific Computing", "Research Engineer", "HPC Engineer", "Data Platform Engineer", ].join(" OR "); /** Customer-facing and early-stage product roles — the consulting lane. */ export const FIELD_KEYWORDS = [ "Forward Deployed Engineer", "Field Engineer", "Customer Engineer", "Sales Engineer", "Pre-Sales Engineer", "Integration Engineer", "API Engineer", "Analytics Engineer", "Founding Engineer", "Product Engineer", ].join(" OR "); // --- Define your searches here --- export const searches: SearchConfig[] = [ { label: "Basel (city)", keywords: KEYWORDS, geoId: "103383283", location: "Basel", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Basel Metropolitan Area", keywords: KEYWORDS, geoId: "90009885", location: "Basel Metropolitan Area", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Basel-Country", keywords: KEYWORDS, geoId: "107117530", location: "Basel-Country", workType: ["onsite", "hybrid"], timePosted: "month", }, { // Name-only, no geoId — deliberately alongside the three geoId searches // above, not instead of them. LinkedIn's guest search serves a *different* // slice per query shape: measured 2026-09-05, the Basel-city geoId query // exhausts at 76 unique cards and never returns some genuinely Basel-Stadt // postings that a name-resolved query does. The PAS01 "Full-Stack Software // Engineer (60-80%)" in Basel (4461764782) was invisible to all six Basel // searches and reached the digest only because the *Bern* query — also // name-only — happened to return it. Overlap with the geoId slice is high // but not total, so this is cheap insurance against exactly that miss. label: "Basel (broad)", keywords: KEYWORDS, location: "Basel, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Bern", keywords: KEYWORDS, location: "Bern, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Zürich", keywords: KEYWORDS, geoId: "102436504", location: "Zürich, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Zürich Metropolitan Area", keywords: KEYWORDS, geoId: "107814425", location: "Zürich Metropolitan Area", workType: ["onsite", "hybrid"], timePosted: "month", }, { label: "Zürich Area", keywords: KEYWORDS, geoId: "90009888", location: "Zürich Area", workType: ["onsite", "hybrid"], timePosted: "month", }, ]; // --- Role-family sweeps --------------------------------------------------- // Same filters as the core searches, run across a narrower set of areas. // Research is Basel/Zürich only (that's where the institutes are); platform // and field roles also sweep Switzerland-wide remote. const CORE_AREAS: { label: string; geoId?: string; location: string }[] = [ { label: "Basel Metropolitan Area", geoId: "90009885", location: "Basel Metropolitan Area" }, { label: "Zürich Metropolitan Area", geoId: "107814425", location: "Zürich Metropolitan Area" }, { label: "Bern", location: "Bern, Switzerland" }, ]; const RESEARCH_AREAS = CORE_AREAS.filter((a) => a.label !== "Bern"); function sweep( family: string, keywords: string, areas: typeof CORE_AREAS, opts: { remote?: boolean } = {} ): SearchConfig[] { const out: SearchConfig[] = areas.map((a) => ({ label: `${family} — ${a.label}`, keywords, geoId: a.geoId, location: a.location, workType: ["onsite", "hybrid"] as WorkType[], timePosted: "month" as TimePosted, })); if (opts.remote) { out.push({ label: `${family} — Switzerland Remote`, keywords, location: "Switzerland", workType: ["remote"], timePosted: "month", }); } return out; } searches.push( ...sweep("Platform", PLATFORM_KEYWORDS, CORE_AREAS, { remote: true }), ...sweep("Research", RESEARCH_KEYWORDS, RESEARCH_AREAS), ...sweep("Field", FIELD_KEYWORDS, CORE_AREAS, { remote: true }) );