/** * The search engines this extension knows how to drive. * * Every selector and every URL parameter below was read off the real SERP in * castle's Chrome, not recalled or inferred. That is not pedantry: Google's own * Tools menu writes `tbs=qdr:*`, which renders an *empty* page on that profile, * while the undocumented-looking `as_qdr=*` works. Anything in here that was not * observed is a bug waiting to be reported as "no results". * * Two extraction modes, because the engines genuinely differ: * * - "items" — the SERP has a clean per-result container (`li.b_algo`, * `.result`, `.snippet[data-type=web]`). Straightforward. * - "headings" — Google has no stable result container, so results are found * from each

outward. Kept as its own mode rather than * forced into the item shape, because it is the one that has * been through the most verification. */ export type EngineId = "google" | "duckduckgo" | "bing" | "brave"; export type Recency = "day" | "week" | "month" | "year"; export const RECENCY_VALUES = ["day", "week", "month", "year"] as const; export const ENGINE_IDS: readonly EngineId[] = ["google", "duckduckgo", "bing", "brave"]; /** Aliases accepted from humans and from the model. */ const ENGINE_ALIASES: Record = { google: "google", g: "google", duckduckgo: "duckduckgo", ddg: "duckduckgo", duck: "duckduckgo", bing: "bing", b: "bing", brave: "brave", }; /** * How the in-page script should read one engine's results. Passed into the * browser as JSON, so everything here must be plain data. */ export type ExtractionConfig = { mode: "items" | "headings"; /** Results container candidates, first match wins. */ roots: string[]; /** "items" mode: one result. */ item?: string; /** Anchor carrying the outbound link, relative to the item. */ link?: string; /** Title element, relative to the item. Falls back to the anchor's text. */ title?: string; /** Description element, relative to the item. */ snippet?: string; /** * "items" mode with no snippet selector: text from these is subtracted from * the item's text to leave the description behind. */ subtract?: string[]; /** Items matching any of these are ads or non-web cards, and are skipped. */ exclude?: string[]; /** How outbound links are wrapped, if they are. */ unwrap: "none" | "ddg" | "bing"; /** Hosts belonging to the engine itself; links to them are not results. */ selfHostPattern: string; }; export type EngineDefinition = { readonly id: EngineId; readonly label: string; /** Human-facing note about what makes this engine worth choosing. */ readonly note: string; /** * Recency windows this engine can actually express, mapped to the parameter * value. A window that is absent is one the engine does not support — never * one that is silently dropped. */ readonly recency: Partial>; readonly buildUrl: (query: string, numResults: number, recency: Recency | undefined) => string; readonly extraction: ExtractionConfig; }; const GOOGLE: EngineDefinition = { id: "google", label: "Google", note: "best result quality; blocks aggressively under repeated automated queries", recency: { day: "d", week: "w", month: "m", year: "y" }, buildUrl: (query, numResults, recency) => { const params = new URLSearchParams({ q: query, num: String(numResults), hl: "en" }); // `udm=14` is the plain "Web" tab: no AI overview, no carousels. But any // date restriction combined with it renders an empty page, and `tbs=qdr:*` // renders an empty page on its own — both observed. So a time-limited // search drops udm and uses the older as_qdr instead. if (recency) params.set("as_qdr", GOOGLE.recency[recency] as string); else params.set("udm", "14"); return `https://www.google.com/search?${params.toString()}`; }, extraction: { mode: "headings", roots: ["#rso", "#search"], unwrap: "none", selfHostPattern: String.raw`^https?://(www\.)?google\.[a-z.]+/`, }, }; const DUCKDUCKGO: EngineDefinition = { id: "duckduckgo", label: "DuckDuckGo", note: "no-JS endpoint, the most stable markup here and the least likely to challenge", // Read off DuckDuckGo's own