How Keyword Location Targeting Works for SERP Data Collection in Open‑SEO
Open‑SEO enables precise keyword location targeting by resolving a country code to a cached list of sub‑country locations, filtering by practical granularities, and routing requests to the appropriate DataForSEO API provider.
Today's SEO tools must account for geographic variance in search results. The Open‑SEO open‑source project implements a multi‑layer system for keyword location targeting that balances accuracy, performance, and cost. This article examines the complete flow—from user query to API routing—based on the actual source code implementation.
Resolving Markets and Validating Requests
Every location search begins with a country code in ISO 3166‑1 alpha‑2 format (e.g., us, gb). The server function searchSerpLocations in src/serverFunctions/serp-locations.ts validates this input using Zod:
// src/serverFunctions/serp-locations.ts
export const searchSerpLocations = createServerFn({ method: "POST" })
.middleware(requireAuthenticatedContext)
.validator(z.object({
query: z.string().min(1).max(100),
countryCode: z.string().regex(/^[a-z]{2}$/i),
}))
.handler(async ({ data }) => {
const all = await fetchSerpLocationsForCountry(data.countryCode);
const needle = data.query.trim().toLowerCase();
return all
.filter(loc => loc.displayLabel.toLowerCase().includes(needle))
.slice(0, 10);
});
This function performs two critical tasks: fetching the full location list via fetchSerpLocationsForCountry, then returning the top 10 autocomplete matches based on the user's typed query.
Fetching and Caching Location Data
The core retrieval logic lives in src/server/lib/dataforseo/serp-locations.ts. This module implements a two‑tier caching strategy to minimize API costs and latency:
- Hot cache check — Query Cloudflare KV with a 1‑day TTL
- Cold fill on miss — Fetch from DataForSEO's Google Locations endpoint and store with 30‑day TTL
// src/server/lib/dataforseo/serp-locations.ts
export async function fetchSerpLocationsForCountry(
countryCode: string,
): Promise<SerpLocationResult[]> {
const iso = countryCode.toLowerCase();
const cached = await env.KV.get(`serp-locations:${iso}`, {
type: "json",
cacheTtl: 24 * 60 * 60,
});
const hit = cachedLocationsSchema.safeParse(cached);
if (hit.success) return hit.data;
return fillFromOrigin(iso);
}
The fillFromOrigin function calls serpApi().googleLocationsCountry to populate the cache when no valid entry exists. A full U.S. location list requires approximately 1.5 MiB of storage.
Filtering to Practical Location Types
Raw DataForSEO responses include many location classifications—some too granular for practical SEO targeting. Open‑SEO filters these to five actionable types defined in INCLUDED_LOCATION_TYPES:
// src/server/lib/dataforseo/serp-locations.ts
const INCLUDED_LOCATION_TYPES = new Set([
"City",
"County",
"Municipality",
"DMA Region",
"Region",
]);
The processing pipeline validates, filters, and transforms each location:
// inside fetchFromDataforseo
return (task.result ?? [])
.map(item => locationItemSchema.safeParse(item))
.flatMap(parsed => (parsed.success ? [parsed.data] : []))
.filter(item => INCLUDED_LOCATION_TYPES.has(item.location_type ?? ""))
.map(item => ({
locationCode: item.location_code,
locationName: item.location_name,
displayLabel: formatLocationLabel(item.location_name),
locationType: item.location_type ?? "",
}));
This ensures users only see location options that will produce meaningful SERP differentiation.
Normalizing Display Labels
Location names from DataForSEO often contain verbose comma‑separated segments. The formatLocationLabel helper in src/shared/keyword-locations.ts cleans these for UI presentation:
// src/shared/keyword-locations.ts
export function formatLocationLabel(name: string): string {
return name
.split(",")
.map(s => s.trim())
.slice(0, 3) // Limit to 3 segments
.join(", ");
}
This truncation prevents unwieldy autocomplete dropdowns while preserving essential geographic context.
Routing to the Correct Data Provider
Not all locations support the same DataForSEO APIs. When a user selects a location for actual keyword data collection, getKeywordDataProvider determines whether to use DataForSEO Labs or the Google Ads API:
// src/shared/keyword-locations.ts
export function getKeywordDataProvider(locationCode: number): KeywordDataProvider {
return LOCATION_CODES.has(locationCode) && !LABS_LOCATION_CODES.has(locationCode)
? "google_ads"
: "labs";
}
This distinction matters because:
- Labs API — Provides rich intent classification, keyword difficulty, and competitive metrics
- Google Ads API — Required for countries where Labs coverage is incomplete (the "Google‑Ads‑only" markets defined in
LOCATION_OPTIONS)
The LOCATION_CODES and LABS_LOCATION_CODES sets encode this geographic coverage matrix, ensuring each request routes to a supported endpoint.
Performance Characteristics
| Aspect | Implementation Detail |
|---|---|
| Cache read latency | Sub‑100ms via Cloudflare KV |
| Cache hit ratio | ~95%+ for common markets |
| API fallback cost | One DataForSEO call per country per 30 days |
| Response size | ~1.5 MiB for U.S., scalable via filtering |
| Autocomplete latency | <50ms for filtered substring matching |
Summary
searchSerpLocationsvalidates country codes and returns ranked autocomplete results from cached location datafetchSerpLocationsForCountryimplements hot‑cache reads with 1‑day TTL and cold‑fill writes with 30‑day TTL- Only five location types—City, County, Municipality, DMA Region, and Region—pass through to users
formatLocationLabeltruncates verbose location names for clean UI presentationgetKeywordDataProviderroutes keyword data requests to Labs or Google Ads APIs based on geographic coverage
Frequently Asked Questions
What happens when a country is not in the cache?
The system triggers fillFromOrigin, which calls the DataForSEO Google Locations endpoint, processes and filters the response, then stores the result in KV with a 30‑day expiration. Subsequent requests serve from cache until expiration.
Why are some location types excluded from results?
DataForSEO returns many granular types—boroughs, neighborhoods, postal codes—that produce negligible SERP variance. The INCLUDED_LOCATION_TYPES whitelist focuses on administrative divisions that materially affect local search results.
How does the provider selection affect my data?
The Labs API provides superior metrics including search intent and keyword difficulty, but lacks coverage in certain countries. When getKeywordDataProvider returns google_ads, you receive volume and competition data from Google's official API instead, with some advanced features unavailable.
Can I customize the cache TTL values?
The current implementation uses hardcoded TTLs: 24 hours for hot reads and 30 days for cold fills in src/server/lib/dataforseo/serp-locations.ts. Modifying these requires editing the source and redeploying.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →