canonry

ADR 0006: Location-Aware Keyword Tracking (Superseded)

Status

Superseded by ADR 0007.

Note

This document proposed a keyword-scoped location model. Canonry’s accepted and implemented model keeps locations project-scoped and uses them as run context. The historical proposal remains below for reference only.

Decision

Add location as a first-class dimension to keyword tracking. Each keyword can be tracked from specific geographic locations (down to city level), and provider APIs receive location hints so search results reflect what users in that location would see.

Why

Data Model Decisions

Verified SDK Support

Provider Param Fields
OpenAI web_search_preview user_location { type: 'approximate', country?, region?, city?, timezone? }
Claude web_search_20250305 user_location { type: 'approximate', country?, region?, city?, timezone? }
Gemini googleSearch None in SDK Prompt-level hint
Local N/A Prompt-level hint

Phase 1: DB Schema + Contracts

New locations table

File: packages/db/src/schema.ts

export const locations = sqliteTable('locations', {
  id: text('id').primaryKey(),
  projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  label: text('label').notNull(),           // "_default" for project default, or auto-generated "Springfield, Illinois, US"
  country: text('country'),                 // ISO 3166-1 alpha-2
  region: text('region'),                   // Free text
  city: text('city'),                       // Free text
  timezone: text('timezone'),               // IANA timezone
  createdAt: text('created_at').notNull(),
}, (table) => [
  index('idx_locations_project').on(table.projectId),
  uniqueIndex('idx_locations_project_label').on(table.projectId, table.label),
])

Add locationId to keywords table

Add locationContext to querySnapshots table

Migration

File: packages/db/src/migrate.ts

  1. CREATE TABLE IF NOT EXISTS locations (...)
  2. Insert _default location per existing project (inherits project country)
  3. ALTER TABLE keywords ADD COLUMN location_id TEXT NOT NULL DEFAULT '_default'
  4. Backfill: UPDATE keywords SET location_id = (SELECT id FROM locations WHERE project_id = keywords.project_id AND label = '_default')
  5. Drop old index, create new (project_id, keyword, location_id) index
  6. ALTER TABLE query_snapshots ADD COLUMN location_context TEXT NOT NULL DEFAULT '{}'

Contract types

File: packages/contracts/src/location.ts (new)

Extend existing contracts


Phase 2: Provider Wiring

All four adapters

Files: packages/provider-*/src/adapter.ts

Each adapter’s executeTrackedQuery (line ~49) manually reconstructs input — must forward new country/region/city/timezone fields. Also update internal *TrackedQueryInput types in each normalize.ts.

OpenAI — native user_location

File: packages/provider-openai/src/normalize.ts

Pass user_location: { type: 'approximate', country, region, city, timezone } to web_search_preview tool config.

Claude — native user_location

File: packages/provider-claude/src/normalize.ts

Same structure on web_search_20250305 tool config (includes timezone).

Gemini — prompt-level hint

File: packages/provider-gemini/src/normalize.ts

Modify buildPrompt() to append (searching from Springfield, Illinois, US) when location provided.

Local — prompt-level hint

File: packages/provider-local/src/normalize.ts

Same prompt-level approach.

Job runner

File: packages/canonry/src/job-runner.ts


Phase 3: API Routes

Location CRUD

File: packages/api-routes/src/locations.ts (new)

Route registration

File: packages/api-routes/src/index.ts — register locationRoutes

Keyword endpoints

File: packages/api-routes/src/keywords.ts

Export endpoint

File: packages/api-routes/src/projects.ts (line 138)

Include spec.locations array and per-keyword location references. Plain string for _default, object for located keywords.

Timeline

File: packages/api-routes/src/history.ts

Add keywordId and locationLabel to timeline response entries. Same keyword with different locations = separate timeline entries (already naturally separated by keywordId).

Snapshot responses

File: packages/api-routes/src/runs.ts

Include parsed locationContext in snapshot responses.


Phase 4: CLI

Location commands

File: packages/canonry/src/commands/location.ts (new)

Keyword commands

File: packages/canonry/src/commands/keyword.ts

Client + CLI registration

Files: packages/canonry/src/client.ts, packages/canonry/src/cli.ts


Phase 5: Web UI

API client types

File: apps/web/src/api.ts

View models

File: apps/web/src/view-models.ts

Dashboard builder — location filter + re-keying

File: apps/web/src/build-dashboard.ts

UI components

File: apps/web/src/App.tsx


Phase 6: Tests

Export roundtrip

File: packages/api-routes/test/export-roundtrip.test.ts

Provider tests

Verify each adapter forwards location fields to API call / prompt.


Consequences

Complete File List

File Phase Changes
packages/db/src/schema.ts 1 locations table, locationId on keywords, locationContext on snapshots
packages/db/src/migrate.ts 1 Migration: create table, backfill, swap index
packages/contracts/src/location.ts 1 New — LocationDto, CreateLocationInput, SnapshotLocationContext
packages/contracts/src/index.ts 1 Export location types
packages/contracts/src/provider.ts 1 Extend TrackedQueryInput
packages/contracts/src/run.ts 1 Extend snapshot DTO
packages/contracts/src/config-schema.ts 1 locations in spec, keyword location references
packages/provider-openai/src/adapter.ts 2 Forward location fields
packages/provider-openai/src/normalize.ts 2 Pass user_location to web_search_preview
packages/provider-claude/src/adapter.ts 2 Forward location fields
packages/provider-claude/src/normalize.ts 2 Pass user_location to web_search_20250305
packages/provider-gemini/src/adapter.ts 2 Forward location fields
packages/provider-gemini/src/normalize.ts 2 Location hint in buildPrompt()
packages/provider-local/src/adapter.ts 2 Forward location fields
packages/provider-local/src/normalize.ts 2 Location hint in buildPrompt()
packages/canonry/src/job-runner.ts 2 Fetch locations, pass to providers, snapshot locationContext
packages/api-routes/src/index.ts 3 Register locationRoutes
packages/api-routes/src/locations.ts 3 New — location CRUD
packages/api-routes/src/keywords.ts 3 Handle locationId in keyword CRUD
packages/api-routes/src/projects.ts 3 Locations in export
packages/api-routes/src/runs.ts 3 locationContext in snapshot response
packages/api-routes/src/history.ts 3 keywordId + locationLabel in timeline
packages/canonry/src/client.ts 4 Location client methods
packages/canonry/src/cli.ts 4 Register location command
packages/canonry/src/commands/location.ts 4 New — location CLI
packages/canonry/src/commands/keyword.ts 4 –location flag
apps/web/src/api.ts 5 Extend types, fetchLocations
apps/web/src/view-models.ts 5 locationId/label, locationScores
apps/web/src/build-dashboard.ts 5 Filter param, re-key on keywordId, locationScores
apps/web/src/App.tsx 5 LocationBadge, breakdown card, filter chips
packages/api-routes/test/export-roundtrip.test.ts 6 Location round-trip test