# How Keyword Location Targeting Works for SERP Data Collection in Open‑SEO

> Learn how Open-SEO uses keyword location targeting to collect SERP data. Discover precise country code resolution and granular filtering for efficient data collection.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-08-10

---

**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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/serp-locations.ts) validates this input using Zod:

```ts
// 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`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/serp-locations.ts). This module implements a **two‑tier caching strategy** to minimize API costs and latency:

1. **Hot cache check** — Query Cloudflare KV with a 1‑day TTL
2. **Cold fill on miss** — Fetch from DataForSEO's Google Locations endpoint and store with 30‑day TTL

```ts
// 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`:

```ts
// 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:

```ts
// 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`](https://github.com/every-app/open-seo/blob/main/src/shared/keyword-locations.ts) cleans these for UI presentation:

```ts
// 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**:

```ts
// 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

- `searchSerpLocations` validates country codes and returns ranked autocomplete results from cached location data
- `fetchSerpLocationsForCountry` implements 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
- `formatLocationLabel` truncates verbose location names for clean UI presentation
- `getKeywordDataProvider` routes 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`](https://github.com/every-app/open-seo/blob/main/src/server/lib/dataforseo/serp-locations.ts). Modifying these requires editing the source and redeploying.