nfidence intervals.
Architecture Decisions
- Separation of Classification and Projection: Classification depends on external SERP data (APIs, crawlers, or manual tagging). Projection depends on internal business logic (CTR ranges, decay curves, volume inputs). Keeping them separate allows you to swap SERP data sources without breaking the forecasting math.
- Range-Based CTR Instead of Point Estimates: SERP features introduce volatility. A single CTR value implies false precision. Using min/max ranges with a configurable confidence weight better reflects real-world variance.
- Position Decay Multipliers: Positions 2β3 do not follow a fixed percentage. They scale relative to the position 1 baseline for that archetype. A multiplier function ensures proportional decay while respecting the archetype's ceiling.
- Extensible Feature Flags: SERP layouts evolve. The architecture uses a bitmask or enum-based feature set so new features (e.g., video carousels, shopping tabs) can be added without refactoring the core projection logic.
Implementation (TypeScript)
// Types & Interfaces
export type SerpFeature = 'AI_OVERVIEW' | 'PAA' | 'SPONSORED_CAROUSEL' | 'FEATURED_SNIPPET';
export interface SerpProfile {
query: string;
features: SerpFeature[];
device: 'desktop' | 'mobile';
intent: 'informational' | 'commercial' | 'navigational';
}
export interface CtrRange {
min: number;
max: number;
}
export interface ArchetypeConfig {
pos1: CtrRange;
pos2_3_multiplier: number; // Applied to pos1 midpoint
}
export interface ProjectionResult {
query: string;
archetype: string;
monthly_volume: number;
target_position: number;
expected_clicks: number;
confidence_interval: [number, number];
}
// Archetype Registry
const ARCHETYPES: Record<string, ArchetypeConfig> = {
PLAIN_ORGANIC: {
pos1: { min: 0.28, max: 0.35 },
pos2_3_multiplier: 0.45,
},
ORGANIC_PAA: {
pos1: { min: 0.18, max: 0.24 },
pos2_3_multiplier: 0.42,
},
ORGANIC_AI_OVERVIEW: {
pos1: { min: 0.08, max: 0.15 },
pos2_3_multiplier: 0.38,
},
ORGANIC_AI_SPONSORED: {
pos1: { min: 0.03, max: 0.06 },
pos2_3_multiplier: 0.35,
},
};
// Classifier Logic (Stub for production SERP API integration)
export function classifySerp(profile: SerpProfile): string {
const hasAI = profile.features.includes('AI_OVERVIEW');
const hasPAA = profile.features.includes('PAA');
const hasSponsored = profile.features.includes('SPONSORED_CAROUSEL');
if (hasAI && hasSponsored) return 'ORGANIC_AI_SPONSORED';
if (hasAI) return 'ORGANIC_AI_OVERVIEW';
if (hasPAA) return 'ORGANIC_PAA';
return 'PLAIN_ORGANIC';
}
// Projection Engine
export function projectTraffic(
profile: SerpProfile,
monthlyVolume: number,
targetPosition: number
): ProjectionResult {
const archetypeKey = classifySerp(profile);
const config = ARCHETYPES[archetypeKey];
const pos1Mid = (config.pos1.min + config.pos1.max) / 2;
let ctr: number;
if (targetPosition === 1) {
ctr = pos1Mid;
} else if (targetPosition >= 2 && targetPosition <= 3) {
ctr = pos1Mid * config.pos2_3_multiplier;
} else {
// Fallback decay for positions 4+
ctr = pos1Mid * Math.pow(0.6, targetPosition - 1);
}
const expectedClicks = Math.round(monthlyVolume * ctr);
const minClicks = Math.round(monthlyVolume * config.pos1.min * (targetPosition === 1 ? 1 : config.pos2_3_multiplier));
const maxClicks = Math.round(monthlyVolume * config.pos1.max * (targetPosition === 1 ? 1 : config.pos2_3_multiplier));
return {
query: profile.query,
archetype: archetypeKey,
monthly_volume: monthlyVolume,
target_position: targetPosition,
expected_clicks: expectedClicks,
confidence_interval: [minClicks, maxClicks],
};
}
Why These Choices Matter
- Enum-based feature detection prevents string mismatch errors and enables TypeScript's type narrowing. When you add a new SERP feature, you update the union type and the classifier logic in one place.
- Multiplier-based decay avoids hardcoding position 2β3 CTRs. If the position 1 baseline shifts due to a new interface update, positions 2β3 scale proportionally, preserving relative distribution.
- Confidence intervals force stakeholders to acknowledge variance. Reporting a single number invites false confidence. A range communicates uncertainty and aligns expectations with real-world SERP volatility.
- Stub classifier design leaves room for production integration. In practice, you would replace
classifySerp with a call to a SERP API, a headless browser renderer, or a cached classification database. The projection engine remains untouched.
Pitfall Guide
1. Point Estimate Fallacy
Explanation: Using a single CTR value (e.g., 12%) ignores SERP volatility, seasonal shifts, and device differences. It creates false precision and breaks when actual performance deviates.
Fix: Always model CTR as a range. Apply a confidence weight (e.g., 0.7 for high certainty, 0.4 for experimental features) to calculate expected values. Report intervals, not absolutes.
2. Desktop-Only Blindness
Explanation: Mobile SERPs render fewer organic results above the fold. Sponsored carousels and AI Overviews occupy more vertical space on small screens. Desktop CTR data systematically overestimates mobile performance.
Fix: Maintain separate CTR ranges for desktop and mobile. If your traffic is >60% mobile, weight your projections accordingly or run parallel forecasts.
3. Intent Drift Misclassification
Explanation: Query intent changes over time. A term that was purely informational in 2023 may trigger commercial carousels in 2026 due to advertiser bidding shifts. Static classifications become stale.
Fix: Schedule periodic SERP re-scans (monthly for high-value terms, quarterly for long-tail). Use automated classification pipelines that flag archetype changes and trigger projection recalculations.
4. Zero-Click Normalization
Explanation: Treating AI Overview answers as purely lost traffic ignores brand exposure, citation clicks, and downstream conversion paths. Users who read the overview may still visit your site for depth, pricing, or implementation details.
Fix: Track zero-click impressions separately. Attribute a fraction of overview visibility to assisted conversions. Adjust CTR ranges downward but add a "citation uplift" factor for authoritative domains.
5. Position Volatility Ignorance
Explanation: Rankings fluctuate daily. A keyword at position 1 today may sit at position 3 next week. Static projections assume rank stability, which rarely exists in competitive niches.
Fix: Model rank as a probability distribution. Use historical rank variance to weight CTR ranges. If a keyword swings between positions 1β3, calculate a blended projection rather than anchoring to a single rank.
6. Archetype Over-Simplification
Explanation: Forcing every query into one of four buckets ignores hybrid SERPs. Some queries trigger AI Overviews, PAA, and featured snippets simultaneously. Rigid classification loses nuance.
Fix: Implement a confidence score for archetype assignment. If multiple features are present, use the most restrictive CTR range (lowest baseline) and flag the query for manual review. Add a HYBRID archetype with conservative defaults.
7. Feedback Loop Absence
Explanation: Projections are useless without validation. Teams that forecast but never compare against actual Search Console data repeat the same errors indefinitely.
Fix: Build a monthly reconciliation pipeline. Compare projected clicks to actual impressions Γ observed CTR. Calculate variance, adjust archetype ranges, and update the classifier weights. Treat forecasting as a continuous calibration process.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Informational query with AI Overview + PAA | Use ORGANIC_AI_OVERVIEW range (8β15% Pos 1) | AI satisfies core intent; PAA consumes scroll depth | Lowers projected traffic; shifts budget to depth/content upgrades |
| Commercial query with sponsored carousel + AI | Use ORGANIC_AI_SPONSORED range (3β6% Pos 1) | Ads and AI dominate viewport; organic visibility is marginal | Reduces organic ROI expectations; increases paid search allocation |
| Navigational or brand query | Use PLAIN_ORGANIC range (28β35% Pos 1) | Users seek specific destination; SERP features rarely interfere | Maintains high projection accuracy; justifies brand defense spend |
| Emerging topic with unstable SERP | Apply HYBRID conservative range + monthly re-scan | Feature set is volatile; early data is unreliable | Adds operational overhead; prevents overcommitment to unproven terms |
Configuration Template
// serf-forecast.config.ts
import { ArchetypeConfig, SerpFeature } from './types';
export const SERP_FEATURES: SerpFeature[] = [
'AI_OVERVIEW',
'PAA',
'SPONSORED_CAROUSEL',
'FEATURED_SNIPPET',
];
export const ARCHETYPE_REGISTRY: Record<string, ArchetypeConfig> = {
PLAIN_ORGANIC: {
pos1: { min: 0.28, max: 0.35 },
pos2_3_multiplier: 0.45,
},
ORGANIC_PAA: {
pos1: { min: 0.18, max: 0.24 },
pos2_3_multiplier: 0.42,
},
ORGANIC_AI_OVERVIEW: {
pos1: { min: 0.08, max: 0.15 },
pos2_3_multiplier: 0.38,
},
ORGANIC_AI_SPONSORED: {
pos1: { min: 0.03, max: 0.06 },
pos2_3_multiplier: 0.35,
},
HYBRID_CONSERVATIVE: {
pos1: { min: 0.05, max: 0.10 },
pos2_3_multiplier: 0.30,
},
};
export const FORECAST_SETTINGS = {
confidence_weight: 0.7,
mobile_weight_adjustment: 0.85, // Apply if >60% mobile traffic
re_scan_interval_days: 30,
variance_threshold: 0.25, // Trigger alert if actual vs projected > 25%
};
Quick Start Guide
- Install the projection engine: Copy the TypeScript implementation into your content planning repository. Export the
projectTraffic function and import the configuration template.
- Classify your keyword list: Run your target queries through a SERP API or manual audit. Tag each with its active features and intent. Map results to the archetype registry.
- Run projections: Pass each classified profile, monthly volume, and target position into
projectTraffic. Capture the expected clicks and confidence intervals.
- Validate and iterate: After 30 days, pull actual Search Console data for the same queries. Calculate variance against your projections. Adjust archetype ranges and confidence weights based on observed performance. Repeat monthly.