@gscdump/analysis 0.38.2 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { num } from "@gscdump/engine/analysis-types";
6
6
  import { err, ok, unwrapResult } from "gscdump/result";
7
7
  import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
8
8
  import { MS_PER_DAY, daysAgoUtc, toIsoDate } from "gscdump/dates";
9
- import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
9
+ import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
10
10
  function rowNumber(value) {
11
11
  if (typeof value === "number") return Number.isFinite(value) ? value : 0;
12
12
  if (typeof value === "bigint") return Number(value);
@@ -522,8 +522,7 @@ function analyzeBrandSegmentation(keywords, options) {
522
522
  const impressions = num(row.impressions);
523
523
  if (impressions < minImpressions) continue;
524
524
  const clicks = num(row.clicks);
525
- const query = row.query.toLowerCase();
526
- if (lowerBrandTerms.some((term) => query.includes(term))) {
525
+ if ((row.variants?.length ? row.variants : [row.query ?? row.keyword ?? ""]).some((query) => lowerBrandTerms.some((term) => query.toLowerCase().includes(term)))) {
527
526
  brand.push(row);
528
527
  brandClicks += clicks;
529
528
  brandImpressions += impressions;
@@ -665,7 +664,7 @@ function createMetricSorter(defaultMetric, orderByMetric) {
665
664
  return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
666
665
  };
667
666
  }
668
- const sortRowResults$1 = createSorter((item, metric) => {
667
+ const sortRowResults = createSorter((item, metric) => {
669
668
  switch (metric) {
670
669
  case "clicks": return item.totalClicks;
671
670
  case "impressions": return item.totalImpressions;
@@ -712,7 +711,7 @@ function analyzeCannibalization(rows, options = {}) {
712
711
  positionSpread
713
712
  });
714
713
  }
715
- return sortRowResults$1(results, sortBy, sortOrder);
714
+ return sortRowResults(results, sortBy, sortOrder);
716
715
  }
717
716
  const cannibalizationAnalyzer = defineAnalyzer({
718
717
  id: "cannibalization",
@@ -2250,14 +2249,9 @@ function shapeDataQueryRows(rows, params, extras) {
2250
2249
  function buildDataDetailPlan(params, options) {
2251
2250
  const state = requireBuilderState(params.q, "data-detail");
2252
2251
  if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
2253
- const main = resolveToSQL(state, options);
2254
- const totals = buildTotalsSql(state, options);
2252
+ const main = resolveToSQLOptimized(state, options);
2255
2253
  const prev = optionalBuilderState(params.qc, "data-detail", "qc");
2256
- const extraQueries = [{
2257
- name: "totals",
2258
- sql: totals.sql,
2259
- params: totals.params
2260
- }];
2254
+ const extraQueries = [];
2261
2255
  if (prev) {
2262
2256
  const previousTotals = buildTotalsSql(prev, options);
2263
2257
  extraQueries.push({
@@ -2275,18 +2269,22 @@ function buildDataDetailPlan(params, options) {
2275
2269
  }
2276
2270
  function shapeDataDetailRows(rows, params, extras) {
2277
2271
  const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
2278
- const coerced = rows.map(coerceNumericCols);
2272
+ const first = rows[0];
2273
+ const totals = {
2274
+ clicks: Number(first?.totalClicks ?? 0),
2275
+ impressions: Number(first?.totalImpressions ?? 0),
2276
+ ctr: Number(first?.totalCtr ?? 0),
2277
+ position: Number(first?.totalPosition ?? 0)
2278
+ };
2279
+ const coerced = rows.map((raw) => {
2280
+ const { totalCount: _tc, totalClicks: _tclk, totalImpressions: _timp, totalCtr: _tctr, totalPosition: _tpos, sum_position: _sp, ...rest } = raw;
2281
+ return coerceNumericCols(rest);
2282
+ });
2279
2283
  const daily = rangeStart && rangeEnd ? padTimeseries(coerced, {
2280
2284
  startDate: rangeStart,
2281
2285
  endDate: rangeEnd
2282
2286
  }) : coerced;
2283
- const totalsRow = extras?.totals?.[0] ?? {};
2284
- const meta = { totals: {
2285
- clicks: Number(totalsRow.clicks ?? 0),
2286
- impressions: Number(totalsRow.impressions ?? 0),
2287
- ctr: Number(totalsRow.ctr ?? 0),
2288
- position: Number(totalsRow.position ?? 0)
2289
- } };
2287
+ const meta = { totals };
2290
2288
  if (extras?.prevTotals) {
2291
2289
  const previousTotalsRow = extras.prevTotals[0] ?? {};
2292
2290
  meta.previousTotals = {
@@ -2403,7 +2401,69 @@ function paginateInMemory(rows, input) {
2403
2401
  const o = clampOffset(input.offset);
2404
2402
  return rows.slice(o, o + l);
2405
2403
  }
2406
- const sortResults$1 = createMetricSorter("lostClicks", {
2404
+ function paginateSortedInMemory(rows, input, compare) {
2405
+ const limit = clampLimit(input.limit, rows.length);
2406
+ const offset = clampOffset(input.offset);
2407
+ const selectedCount = Math.min(rows.length, offset + limit);
2408
+ if (limit === 0 || offset >= rows.length || selectedCount === 0) return [];
2409
+ const fullSort = () => [...rows].sort(compare).slice(offset, offset + limit);
2410
+ if (selectedCount >= rows.length / 2) return fullSort();
2411
+ let invalidComparison = false;
2412
+ const compareStable = (left, right) => {
2413
+ const order = compare(left.value, right.value);
2414
+ if (Number.isNaN(order)) {
2415
+ invalidComparison = true;
2416
+ return left.index - right.index;
2417
+ }
2418
+ return order || left.index - right.index;
2419
+ };
2420
+ const isWorse = (left, right) => compareStable(left, right) > 0;
2421
+ const heap = [];
2422
+ const siftUp = (start) => {
2423
+ let index = start;
2424
+ while (index > 0) {
2425
+ const parent = index - 1 >>> 1;
2426
+ if (!isWorse(heap[index], heap[parent])) break;
2427
+ const swap = heap[parent];
2428
+ heap[parent] = heap[index];
2429
+ heap[index] = swap;
2430
+ index = parent;
2431
+ }
2432
+ };
2433
+ const siftDown = () => {
2434
+ let index = 0;
2435
+ for (;;) {
2436
+ const left = index * 2 + 1;
2437
+ if (left >= heap.length) return;
2438
+ const right = left + 1;
2439
+ let worse = left;
2440
+ if (right < heap.length && isWorse(heap[right], heap[left])) worse = right;
2441
+ if (!isWorse(heap[worse], heap[index])) return;
2442
+ const swap = heap[index];
2443
+ heap[index] = heap[worse];
2444
+ heap[worse] = swap;
2445
+ index = worse;
2446
+ }
2447
+ };
2448
+ for (let index = 0; index < rows.length; index++) {
2449
+ const candidate = {
2450
+ value: rows[index],
2451
+ index
2452
+ };
2453
+ if (heap.length < selectedCount) {
2454
+ heap.push(candidate);
2455
+ siftUp(heap.length - 1);
2456
+ } else if (compareStable(candidate, heap[0]) < 0) {
2457
+ heap[0] = candidate;
2458
+ siftDown();
2459
+ }
2460
+ }
2461
+ if (invalidComparison) return fullSort();
2462
+ heap.sort(compareStable);
2463
+ if (invalidComparison) return fullSort();
2464
+ return heap.slice(offset, offset + limit).map((entry) => entry.value);
2465
+ }
2466
+ const sortResults = createMetricSorter("lostClicks", {
2407
2467
  lostClicks: "desc",
2408
2468
  declinePercent: "desc",
2409
2469
  currentClicks: "asc"
@@ -2436,7 +2496,7 @@ function analyzeDecay(input, options = {}) {
2436
2496
  positionDrop: currentPosition != null ? currentPosition - prev.position : null
2437
2497
  });
2438
2498
  }
2439
- return sortResults$1(results, sortBy);
2499
+ return sortResults(results, sortBy);
2440
2500
  }
2441
2501
  const decayAnalyzer = defineAnalyzer({
2442
2502
  id: "decay",
@@ -3435,12 +3495,52 @@ function calculateCtrGapScore(actualCtr, position) {
3435
3495
  const gap = expectedCtr - actualCtr;
3436
3496
  return Math.min(gap / expectedCtr, 1);
3437
3497
  }
3438
- const sortResults = createMetricSorter("opportunityScore", {
3498
+ const SORT_DIR = {
3439
3499
  opportunityScore: "desc",
3440
3500
  potentialClicks: "desc",
3441
3501
  impressions: "desc",
3442
3502
  position: "asc"
3443
- });
3503
+ };
3504
+ function analyzeOpportunity(keywords, options = {}) {
3505
+ const minImpressions = options.minImpressions ?? 100;
3506
+ const positionWeight = options.weights?.position ?? 1;
3507
+ const impressionsWeight = options.weights?.impressions ?? 1;
3508
+ const ctrGapWeight = options.weights?.ctrGap ?? 1;
3509
+ const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
3510
+ const sortBy = options.sortBy ?? "opportunityScore";
3511
+ const results = [];
3512
+ for (const row of keywords) {
3513
+ const impressions = num(row.impressions);
3514
+ if (impressions < minImpressions) continue;
3515
+ const position = num(row.position);
3516
+ const ctr = num(row.ctr);
3517
+ const clicks = num(row.clicks);
3518
+ const positionScore = calculatePositionScore(position);
3519
+ const impressionScore = calculateImpressionScore(impressions);
3520
+ const ctrGapScore = calculateCtrGapScore(ctr, position);
3521
+ const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
3522
+ const opportunityScore = Math.round(weightedProduct ** (1 / totalWeight) * 100);
3523
+ const potentialClicks = Math.round(impressions * getExpectedCtr(Math.min(3, position)));
3524
+ results.push({
3525
+ keyword: row.query,
3526
+ page: row.page ?? null,
3527
+ clicks,
3528
+ impressions,
3529
+ ctr,
3530
+ position,
3531
+ opportunityScore,
3532
+ potentialClicks,
3533
+ factors: {
3534
+ positionScore,
3535
+ impressionScore,
3536
+ ctrGapScore
3537
+ }
3538
+ });
3539
+ }
3540
+ const direction = SORT_DIR[sortBy];
3541
+ results.sort((left, right) => direction === "asc" ? left[sortBy] - right[sortBy] : right[sortBy] - left[sortBy]);
3542
+ return options.limit ? results.slice(0, options.limit) : results;
3543
+ }
3444
3544
  const opportunityAnalyzer = defineAnalyzer({
3445
3545
  id: "opportunity",
3446
3546
  buildSql(params) {
@@ -3554,51 +3654,15 @@ const opportunityAnalyzer = defineAnalyzer({
3554
3654
  return { queries: queriesQueryState(periodOf(params), params.limit) };
3555
3655
  },
3556
3656
  reduceRows(rows, params) {
3557
- const keywords = (Array.isArray(rows) ? rows : []) ?? [];
3558
- const minImpressions = params.minImpressions ?? 100;
3559
- const positionWeight = 1;
3560
- const impressionsWeight = 1;
3561
- const ctrGapWeight = 1;
3562
- const sortBy = "opportunityScore";
3563
- const results = [];
3564
- for (const row of keywords) {
3565
- const impressions = num(row.impressions);
3566
- const position = num(row.position);
3567
- const ctr = num(row.ctr);
3568
- const clicks = num(row.clicks);
3569
- if (impressions < minImpressions) continue;
3570
- const positionScore = calculatePositionScore(position);
3571
- const impressionScore = calculateImpressionScore(impressions);
3572
- const ctrGapScore = calculateCtrGapScore(ctr, position);
3573
- const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
3574
- const opportunityScore = Math.round(geometricMean * 100);
3575
- const targetCtr = getExpectedCtr(Math.min(3, position));
3576
- const potentialClicks = Math.round(impressions * targetCtr);
3577
- results.push({
3578
- keyword: row.query,
3579
- page: row.page ?? null,
3580
- clicks,
3581
- impressions,
3582
- ctr,
3583
- position,
3584
- opportunityScore,
3585
- potentialClicks,
3586
- factors: {
3587
- positionScore,
3588
- impressionScore,
3589
- ctrGapScore
3590
- }
3591
- });
3592
- }
3593
- const sorted = sortResults(results, sortBy);
3594
- const paged = paginateInMemory(sorted, {
3657
+ const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
3658
+ const paged = paginateSortedInMemory(results, {
3595
3659
  limit: params.limit,
3596
3660
  offset: params.offset
3597
- });
3661
+ }, (left, right) => right.opportunityScore - left.opportunityScore);
3598
3662
  return {
3599
3663
  results: paged,
3600
3664
  meta: {
3601
- total: sorted.length,
3665
+ total: results.length,
3602
3666
  returned: paged.length
3603
3667
  }
3604
3668
  };
@@ -4270,6 +4334,12 @@ const stlDecomposeAnalyzer = defineAnalyzer({
4270
4334
  }
4271
4335
  });
4272
4336
  const DEFAULT_ROW_LIMIT$1 = 25e3;
4337
+ function paginateStrikingDistance(results, options) {
4338
+ return paginateSortedInMemory(results, {
4339
+ limit: options.limit ?? 1e3,
4340
+ offset: options.offset
4341
+ }, (left, right) => right.potentialClicks - left.potentialClicks);
4342
+ }
4273
4343
  function filterStrikingDistance(rows, options = {}) {
4274
4344
  const minPosition = options.minPosition ?? 4;
4275
4345
  const maxPosition = options.maxPosition ?? 20;
@@ -4300,11 +4370,7 @@ const strikingDistanceAnalyzer = defineAnalyzer({
4300
4370
  id: "striking-distance",
4301
4371
  reduce(rows, params) {
4302
4372
  const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
4303
- results.sort((a, b) => b.potentialClicks - a.potentialClicks);
4304
- const paged = paginateInMemory(results, {
4305
- limit: params.limit ?? 1e3,
4306
- offset: params.offset
4307
- });
4373
+ const paged = paginateStrikingDistance(results, params);
4308
4374
  return {
4309
4375
  results: paged,
4310
4376
  meta: {
@@ -4680,7 +4746,18 @@ const trendsAnalyzer = defineAnalyzer({
4680
4746
  }
4681
4747
  });
4682
4748
  const DEFAULT_ROW_LIMIT = 25e3;
4683
- const sortRowResults = createSorter((item) => item.impressions, "impressions");
4749
+ function analyzeZeroClick(rows, options = {}) {
4750
+ const minImpressions = options.minImpressions ?? 1e3;
4751
+ const maxCtr = options.maxCtr ?? .03;
4752
+ const maxPosition = options.maxPosition ?? 10;
4753
+ const queryMap = /* @__PURE__ */ new Map();
4754
+ for (const row of rows) {
4755
+ if (row.impressions < minImpressions || row.position > maxPosition || row.ctr > maxCtr) continue;
4756
+ const existing = queryMap.get(row.query);
4757
+ if (!existing || row.position < existing.position) queryMap.set(row.query, { ...row });
4758
+ }
4759
+ return [...queryMap.values()].sort((left, right) => right.impressions - left.impressions);
4760
+ }
4684
4761
  const defaultAnalyzerRegistry = createAnalyzerRegistry({ defined: [
4685
4762
  bayesianCtrAnalyzer,
4686
4763
  bipartitePagerankAnalyzer,
@@ -4793,30 +4870,15 @@ const defaultAnalyzerRegistry = createAnalyzerRegistry({ defined: [
4793
4870
  return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
4794
4871
  },
4795
4872
  reduceRows(rows, params) {
4796
- const arr = Array.isArray(rows) ? rows : [];
4797
- const minImpressions = params.minImpressions ?? 1e3;
4798
- const maxCtr = params.maxCtr ?? .03;
4799
- const maxPosition = params.maxPosition ?? 10;
4800
- const queryMap = /* @__PURE__ */ new Map();
4801
- for (const row of arr) {
4802
- if (row.impressions < minImpressions) continue;
4803
- if (row.position > maxPosition) continue;
4804
- if (row.ctr > maxCtr) continue;
4805
- const existing = queryMap.get(row.query);
4806
- if (!existing || row.position < existing.position) queryMap.set(row.query, {
4807
- query: row.query,
4808
- page: row.page,
4809
- clicks: row.clicks,
4810
- impressions: row.impressions,
4811
- ctr: row.ctr,
4812
- position: row.position
4813
- });
4814
- }
4815
- const results = sortRowResults(Array.from(queryMap.values()), "impressions", "desc");
4816
- const paged = paginateInMemory(results, {
4873
+ const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
4874
+ minImpressions: params.minImpressions ?? 1e3,
4875
+ maxCtr: params.maxCtr ?? .03,
4876
+ maxPosition: params.maxPosition ?? 10
4877
+ });
4878
+ const paged = paginateSortedInMemory(results, {
4817
4879
  limit: params.limit,
4818
4880
  offset: params.offset
4819
- });
4881
+ }, (left, right) => right.impressions - left.impressions);
4820
4882
  return {
4821
4883
  results: paged,
4822
4884
  meta: {
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Analyzer, Analyzer as Analyzer$1, AnalyzerCapabilityError, AnalyzerRegistry, AnalyzerRegistry as AnalyzerRegistry$1, AnalyzerRegistryInit, AnalyzerVariants, DefineAnalyzerOptions, DefinedAnalyzer, Plan, ReduceContext, ReduceCtx, Reducer, RequiredCapability, RowQueriesPlan, SqlExtraQuery, SqlPlan, SqlPlanSpec, TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
2
- import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow, windowToComparisonPeriod, windowToPeriod } from "@gscdump/engine/period";
2
+ import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
3
3
  import { AnalysisParams, AnalysisParams as AnalysisParams$1, AnalysisResult, AnalysisResult as AnalysisResult$1, AnalysisTool, num } from "@gscdump/engine/analysis-types";
4
4
  import { Result } from "gscdump/result";
5
5
  import { BuilderState } from "gscdump/query";
6
- import { AnalysisQuerySource, AnalysisQuerySource as AnalysisQuerySource$1, AnalysisSourceKind, AttachedTableRunner, AttachedTableRunner as AnalyzerRunner, AttachedTableSourceOptions, AttachedTableSourceOptions as BrowserAnalyzeOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, QueryRow as QueryRow$1, SourceCapabilities, TypedQuery, createEngineQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery } from "@gscdump/engine/source";
6
+ import { AnalysisQuerySource, AnalysisQuerySource as AnalysisQuerySource$1, AnalysisSourceKind, AttachedTableRunner, AttachedTableRunner as AnalyzerRunner, AttachedTableSourceOptions, AttachedTableSourceOptions as BrowserAnalyzeOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, QueryRow as QueryRow$1, SourceCapabilities, createEngineQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine } from "@gscdump/engine/source";
7
7
  import { DefineReportOptions, DefinedReport, DefinedReport as DefinedReport$1, ReportAction, ReportContext, ReportContext as ReportContext$1, ReportFinding, ReportParams, ReportPlanStep, ReportResult, ReportResult as ReportResult$1, ReportSection } from "@gscdump/engine/report";
8
8
  import { PlannerCapabilities } from "gscdump/query/plan";
9
9
  type ActionSource = 'cannibalization' | 'striking-distance' | 'ctr-anomaly' | 'change-point' | 'opportunity';
@@ -78,6 +78,13 @@ interface BrandSegmentationOptions {
78
78
  /** Minimum impressions for a keyword to be included. Default: 10 */
79
79
  minImpressions?: number;
80
80
  }
81
+ interface BrandSegmentationRow {
82
+ query?: string;
83
+ keyword?: string;
84
+ variants?: readonly string[];
85
+ clicks?: number | null;
86
+ impressions?: number | null;
87
+ }
81
88
  interface BrandSummary {
82
89
  brandClicks: number;
83
90
  nonBrandClicks: number;
@@ -85,16 +92,17 @@ interface BrandSummary {
85
92
  brandImpressions: number;
86
93
  nonBrandImpressions: number;
87
94
  }
88
- interface BrandSegmentationResult {
89
- brand: QueriesRow[];
90
- nonBrand: QueriesRow[];
95
+ interface BrandSegmentationResult<T extends BrandSegmentationRow = QueriesRow> {
96
+ brand: T[];
97
+ nonBrand: T[];
91
98
  summary: BrandSummary;
92
99
  }
93
100
  /**
94
101
  * Pure helper: segment keywords into brand and non-brand based on provided
95
102
  * brand terms. Re-exported from `@gscdump/analysis` for portable callers.
96
103
  */
97
- declare function analyzeBrandSegmentation(keywords: QueriesRow[], options: BrandSegmentationOptions): BrandSegmentationResult;
104
+ declare function analyzeBrandSegmentation(keywords: QueriesRow[], options: BrandSegmentationOptions): BrandSegmentationResult<QueriesRow>;
105
+ declare function analyzeBrandSegmentation<T extends BrandSegmentationRow>(keywords: T[], options: BrandSegmentationOptions): BrandSegmentationResult<T>;
98
106
  type CannibalizationSortMetric = 'clicks' | 'impressions' | 'positionSpread' | 'pageCount';
99
107
  interface CannibalizationOptions {
100
108
  /** Minimum impressions for a query to be considered. Default: 10 */
@@ -295,6 +303,7 @@ interface MoversResult {
295
303
  * recent changes between two periods.
296
304
  */
297
305
  declare function analyzeMovers(input: MoversInput, options?: MoversOptions): MoversResult;
306
+ type OpportunitySortMetric = 'opportunityScore' | 'potentialClicks' | 'impressions' | 'position';
298
307
  interface OpportunityFactors {
299
308
  positionScore: number;
300
309
  impressionScore: number;
@@ -311,6 +320,19 @@ interface OpportunityResult {
311
320
  potentialClicks: number;
312
321
  factors: OpportunityFactors;
313
322
  }
323
+ interface OpportunityWeights {
324
+ position?: number;
325
+ impressions?: number;
326
+ ctrGap?: number;
327
+ }
328
+ interface OpportunityOptions {
329
+ minImpressions?: number;
330
+ weights?: OpportunityWeights;
331
+ sortBy?: OpportunitySortMetric;
332
+ limit?: number;
333
+ }
334
+ /** Pure opportunity scorer shared by hosted and package analyzer callers. */
335
+ declare function analyzeOpportunity(keywords: QueriesRow[], options?: OpportunityOptions): OpportunityResult[];
314
336
  type SeasonalityMetric = 'clicks' | 'impressions';
315
337
  interface SeasonalityOptions {
316
338
  /** Metric to analyze for seasonality. Default: clicks */
@@ -371,12 +393,25 @@ interface StrikingDistanceResult {
371
393
  /** Estimated clicks at ~15% CTR (the average for positions 1–3). */
372
394
  potentialClicks: number;
373
395
  }
374
- /**
375
- * `zero-click` — high impressions, low CTR, good ranking. SQL groups by
376
- * (query, url) and applies HAVING/WHERE pushdown; row reducer dedupes to the
377
- * best-positioned page per query (different semantics, kept for parity with
378
- * the existing live-GSC behavior).
379
- */
396
+ interface StrikingDistanceFilterOptions {
397
+ minPosition?: number;
398
+ maxPosition?: number;
399
+ minImpressions?: number;
400
+ maxCtr?: number;
401
+ }
402
+ interface StrikingDistanceOptions extends StrikingDistanceFilterOptions {
403
+ limit?: number;
404
+ offset?: number;
405
+ }
406
+ /** Pure striking-distance analysis for consumers that already own the rows. */
407
+ declare function analyzeStrikingDistance(rows: readonly {
408
+ query: unknown;
409
+ page?: unknown;
410
+ clicks: unknown;
411
+ impressions: unknown;
412
+ ctr: unknown;
413
+ position: unknown;
414
+ }[], options?: StrikingDistanceOptions): StrikingDistanceResult[];
380
415
  interface ZeroClickResult {
381
416
  query: string;
382
417
  page: string;
@@ -385,6 +420,13 @@ interface ZeroClickResult {
385
420
  ctr: number;
386
421
  position: number;
387
422
  }
423
+ interface ZeroClickOptions {
424
+ minImpressions?: number;
425
+ maxCtr?: number;
426
+ maxPosition?: number;
427
+ }
428
+ /** Pure zero-click detector shared by hosted and package analyzer callers. */
429
+ declare function analyzeZeroClick(rows: QueryPageRow[], options?: ZeroClickOptions): ZeroClickResult[];
388
430
  declare function analyzeInBrowser(runner: AttachedTableRunner, opts: AttachedTableSourceOptions, params: AnalysisParams$1, registry: AnalyzerRegistry$1): Promise<AnalysisResult$1>;
389
431
  /**
390
432
  * Default analyzer registry built from every in-tree analyzer. Convenience
@@ -604,4 +646,4 @@ interface InMemoryQuerySourceOptions {
604
646
  }
605
647
  declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource$1;
606
648
  declare const SQL_ANALYZERS: readonly Analyzer$1[];
607
- export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, DEFAULT_PRIORITY_SOURCES, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type Effort, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityResult, type PadTimeseriesOptions, type PageRow, type Plan, type PriorityAction, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportAction, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceResult, type TypedQuery, type TypedRowQuery, type WindowPreset, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, classifyQueryIntent, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, decodeIntent, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryComparisonRows, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions, typedQuery, windowToComparisonPeriod, windowToPeriod };
649
+ export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, DEFAULT_PRIORITY_SOURCES, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type Effort, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type PriorityAction, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportAction, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, decodeIntent, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions };