# Geo

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 35-46 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 47-58 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 59-69 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 70-100 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 101-116 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 117-128 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 129-166 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 167-175 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 176-221 |
| [Examples](#examples) | Representative usage patterns and code. | 222-268 |
| [Verification](#verification) | Evidence required before reporting success. | 269-280 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 281-290 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 291-299 |
<!-- END GENERATED: guide-map -->

Geo owns geography containment over already-acquired rows. Use it when a row has
free-form location text and you need to decide whether that row sits inside one
target geography such as a region grouping, country, state/admin1, named metro,
or city.

It does not discover places, enrich addresses, infer headquarters, or activate
sequence rows. Those belong to narrower source, enrichment, workflow, and
sequence references.

## Purpose

`vf.rows.geo_classify(rows, target=...)` starts a metered artifact job that
adds a containment verdict and audit columns to each processed row. It is the
right tool for filters like "Bay Area", "LATAM", "EMEA", "United States", or
"is this person in the target metro?" when the input location is messy text.

`vf.rows.geo_supported_targets()` is the read-only discovery call for named
metros and region groupings accepted by the classifier. Countries, country
codes, globally unique admin1 names, and dominant city names can resolve
dynamically and are not fully enumerated by the discovery call.

## Use This When

- You have free-form location strings such as `"Palo Alto, CA"`, `"remote,
  mostly Austin"`, `"engineering manager based out of Berlin"`, or `"SF Bay"`.
- You need a single boolean-ish geography filter before enrichment, reporting,
  or sequence/source staging.
- You need auditability: why a row matched, what specificity resolved, whether
  an error occurred, and how many credits that row used.
- You need named metro support rather than exact city/state matching.
- You need to guard against ambiguous target names such as `Georgia` or
  non-dominant city names.

## Do Not Use This For

| Task | Use Instead |
| --- | --- |
| Exact structured filters like `country_code == "US"` or `state == "CA"` | Local `RowSet`/Python filters from `row_operations.md` |
| Extracting `city`, `state`, or `country` fields from text | `vf.rows.extract(...)` in `row_operations.md` |
| Finding company HQ or postal address from a domain | `enrichment.md` address/company surfaces |
| Discovering local businesses or places | `places.md` |
| Provider-native location filters for people/account search | The relevant source/provider reference |
| Sequence/source staging or publish decisions after filtering | `sequences.md` and `safety.md` |

## Mental Model

Geo target resolution is strict. The backend resolves the single `target`
string in this order:

1. Region groupings and continents: `LATAM`, `Middle East`, `EMEA`, `APAC`,
   `Americas`, `Africa`, `Antarctica`, `Asia`, `Europe`, `North America`,
   `Oceania`, `South America`.
2. Named metros by catalog name or alias from
   `vf.rows.geo_supported_targets()`.
3. Countries by common name, ISO2, or ISO3 code.
4. Globally unique admin1 names, such as a state/province name that does not
   collide globally.
5. Dominant city names when the top GeoNames match is at least 10x larger than
   the runner-up.

Ambiguous targets fail fast before row processing. `Georgia` is both a country
and a US state, so it is rejected instead of silently choosing the wrong one.
For the country, use `GE` or `GEO`. Do not rely on comma-qualified target forms
such as city plus state or country plus disambiguator; that parser does not
exist today.

Row classification is a resolver-plan capability. Each processed row has a
baseline cost. Local geo tables can avoid the LLM surcharge, but they do not
make the row free. Ambiguous row text may use one or two LLM fallback calls:
one classifier over candidate metros and one extractor for city/state/country.

The backend classifies rows concurrently inside a single artifact job, but the
SDK call uploads at most 500 rows. For larger inputs, deliberately chunk with
`start_row`/`max_rows` or use a workflow step that owns chunking and merging.

## Preflight

1. Confirm execution authority before calling the API. `geo_supported_targets`
   is read-only; `geo_classify` is metered.
2. If the target is a named metro or region grouping, call
   `vf.rows.geo_supported_targets()` and inspect aliases before running a job.
3. If the target is ambiguous, choose an actually supported unambiguous form
   before starting the job.
4. Inspect a small sample of input rows and choose `text_columns` explicitly
   when possible. Auto-detection chooses text-shaped columns, but it cannot know
   intent.
5. For structured columns, prefer deterministic local filtering instead of a
   metered geo job.
6. For more than 500 rows, decide how chunks will be paged, merged, and
   audited before starting the first job.

## Risk And Side Effects

| Surface | Side Effect | Notes |
| --- | --- | --- |
| `vf.rows.geo_supported_targets()` | `readonly_api` | Authenticated read, no credits. |
| `vf.rows.geo_classify(...)` | `artifact_job` | Uploads row input, creates a backend job/artifact, consumes Growth credits. |

Geo classification does not mutate the original rows, CRM, sequences, or
senders. It only writes an output artifact. Any downstream sequence/source
staging, publish, send, or purchase still follows `safety.md` and the relevant
sequence/sender/domain reference.

## Canonical Operating Pattern

1. Pick the exact target and prove it resolves.
2. Run a bounded sample first when the row shape, target, or cost is uncertain.
3. Preserve `job_id` and `artifact_id`.
4. Inspect verdict counts plus audit/error columns.
5. Filter only after inspecting `unknown` and error rates.
6. For large inputs, process explicit chunks and merge artifacts/rows
   intentionally.

```python
import verbiflow as vf

rows = [
    {"name": "Ada", "location": "San Francisco, CA"},
    {"name": "Beto", "location": "remote, mostly Palo Alto"},
    {"name": "Carla", "location": "Berlin"},
]

targets = vf.rows.geo_supported_targets()
print([metro["label"] for metro in targets["metros"][:10]])

job = vf.rows.geo_classify(
    rows,
    target="SF Bay Area",
    text_columns=["location"],
    output_column="in_sf_bay",
    wait=True,
)
result = vf.jobs.result(job).wait(require_success=True)
classified = result.rows(limit=100)

verdicts = {"true": 0, "false": 0, "unknown": 0}
for row in classified:
    verdicts[str(row.get("in_sf_bay") or "unknown")] += 1
print(verdicts)
```

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.rows.geo_classify` | Use to filter rows by whether their location text falls inside a target geography (continent, country, US state/admin1, named metro, or dominant city). Local geo tables can avoid the LLM surcharge, but every processed row still has the resolver-plan baseline. Use `rows.geo_supported_targets` first if you want to see which named metros are recognized. | <strong>Required</strong>: `rows_or_artifact` (Iterable[Mapping[str, object]] \| str \| Path \| Artifact \| RowSet; required): Rows, RowSet, artifact id, Artifact, or absolute CSV path.; `target` (str; required): Target geography to test each row against. Accepts a continent or region (Americas, EMEA, APAC, LATAM, Middle East), a country (name or ISO code), an unambiguous state/admin1 name, a named metro (call `rows.geo_supported_targets` to see the catalog), or a dominant city name.<br><strong>Optional</strong>: `max_rows` (int; optional; default `500`): Maximum rows to upload, process, or return for this call.; `output_column` (str; optional; default `'geo_in_target'`): Column name written with 'true' / 'false' / 'unknown' per row. Geo audit columns (`geo_classification_reason`, `geo_resolved_level`, `geo_classification_error`, `geo_credit_units`) are added alongside.; `start_row` (int; optional; default `1`): One-based first row to upload/process from a CSV or artifact.; `text_columns` (list[str] \| None; optional; default `None`): Columns to concatenate as the location text per row. Omit to auto-detect the longest text-shaped column.; `total_rows` (int \| None; optional; default `None`): Total rows in the source dataset. Required only when you pass a pre-sliced chunk with `start_row > 1`; the SDK auto-discovers this for Iterable, RowSet, Path, and Artifact inputs.; `wait` (bool \| int; optional; default `False`): If false, return immediately with a job/status handle; if true or a timeout integer, poll until completion or timeout.<br><strong>Note</strong>: `max_rows` is capped by the SDK at 500 for one upload/job chunk. `output_column` controls only the verdict column; fixed geo audit columns are always added. | `PendingJob` whose output artifact adds `output_column` ('true'/'false'/'unknown' per row), `geo_classification_reason` (human-readable explanation), `geo_resolved_level` (metro/city/admin1/country/ambiguous/unknown), `geo_classification_error`, and per-row `geo_credit_units`.<br><strong>Inspect</strong>: Inspect the verdict column, `geo_classification_reason`, `geo_resolved_level`, `geo_classification_error`, and `geo_credit_units` before filtering or staging.<br><strong>Common next calls</strong>: `vf.jobs.get`, `vf.jobs.stream_rows`, `vf.artifacts.get`, `vf.jobs.export_csv` | artifact_job | installed SDK: `verbiflow/rows.py:662-789` |
| `vf.rows.geo_supported_targets` | Use to discover the named metros and regional groupings that `rows.geo_classify` accepts as a target. Free, read-only — does not consume credits. | No inputs.<br><strong>Note</strong>: This is an authenticated read. It lists named metros and region groupings only; countries, ISO codes, globally unique admin1 names, and dominant city names resolve dynamically. | `dict` with `metros` (named metros + their aliases) and `regions` (continents + groupings). Note that countries, US states, and city names are accepted dynamically and not listed here.<br><strong>Inspect</strong>: Public metro rows use `label`, `kind`, `country_code`, `admin1_code`, and `aliases`; region rows use `label`, `kind`, and `aliases`.<br><strong>Common next calls</strong>: `vf.rows.geo_classify` | readonly_api | installed SDK: `verbiflow/rows.py:791-815` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

### Supported Targets

`vf.rows.geo_supported_targets()` returns the public API shape:

```json
{
  "metros": [
    {
      "label": "San Francisco Bay Area",
      "kind": "metro",
      "country_code": "US",
      "admin1_code": "CA",
      "aliases": ["Bay Area", "SF Bay Area", "Silicon Valley"]
    }
  ],
  "regions": [
    {
      "label": "latam",
      "kind": "country_set",
      "aliases": ["AR", "BO", "BR"]
    }
  ],
  "note": "Country names, country ISO codes, unambiguous state/admin1 names, and dominant city names are accepted dynamically and not listed here."
}
```

Do not assume this call lists every valid country, state/admin1, or city target.
Use it for named metros and broad region groupings.

### Geo Output Columns

`vf.rows.geo_classify(...)` returns a `PendingJob`. The completed artifact adds:

| Column | Values | Meaning |
| --- | --- | --- |
| `output_column` argument, default `geo_in_target` | `"true"`, `"false"`, `"unknown"` | Containment verdict. |
| `geo_classification_reason` | string | Human-readable explanation for the verdict. |
| `geo_resolved_level` | `metro`, `city`, `admin1`, `country`, `ambiguous`, `unknown` | Specificity of the resolved row location. |
| `geo_classification_error` | string or empty | Backend/provider error for the row, if any. |
| `geo_credit_units` | decimal string | Per-row credits recorded by the resolver plan. |

The output artifact preserves the input columns and appends these fields unless
the input already had a colliding column name.

## Examples

### Region Filter

```python
job = vf.rows.geo_classify(
    rows,
    target="LATAM",
    text_columns=["location"],
    output_column="in_latam",
    wait=True,
)
latam_rows = [
    row
    for row in vf.jobs.result(job).wait(require_success=True).rows(limit=500)
    if row.get("in_latam") == "true"
]
```

### More Than 500 Rows

```python
all_rows = []
start_row = 1
csv_path = "/absolute/path/to/input.csv"

while True:
    job = vf.rows.geo_classify(
        csv_path,
        target="SF Bay Area",
        text_columns=["location"],
        start_row=start_row,
        max_rows=500,
        wait=True,
    )
    page = vf.jobs.result(job).wait(require_success=True).rows(limit=500)
    if not page:
        break
    all_rows.extend(page)
    if len(page) < 500:
        break
    start_row += 500
```

For production workflow code, put this chunking in a workflow step so checkpoint
state, artifacts, reports, and credit usage are visible.

## Verification

- Print the target, row count, `job_id`, and `artifact_id`.
- Count `true` / `false` / `unknown` verdicts.
- Sample rows from each verdict class and inspect `geo_classification_reason`.
- Inspect `geo_classification_error` before treating `unknown` as a genuine
  non-match.
- Sum or sample `geo_credit_units` when explaining run cost, but use billing
  surfaces for authoritative workspace spend.
- Before staging filtered rows to a sequence/source, re-check safety and
  sequence references.

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `target 'Georgia' matches both a country and an admin1` | Ambiguous target | Use `GE` or `GEO` for the country, or choose another accepted unambiguous target. |
| `target city 'Springfield' is ambiguous` | No dominant city candidate | Use a broader supported target, a structured local filter, or extend backend target parsing before documenting comma-qualified targets. |
| Many `unknown` verdicts | Wrong `text_columns`, sparse location data, or failed fallback calls | Inspect row samples, `geo_classification_error`, and input columns before filtering. |
| Only 500 rows processed | SDK call cap | Chunk explicitly with `start_row`/`max_rows` or move the work into a workflow step. |
| Credits higher than expected | Ambiguous text triggered LLM fallbacks | Inspect `geo_credit_units` and reasons; improve source location fields when possible. |

## Related References

- `row_operations.md` owns local row shaping, filtering, `RowSet`, and CSV work.
- `artifacts_jobs.md` owns job waiting, artifact paging, streaming, and export.
- `parallelism.md` owns async and batching patterns for multiple SDK calls.
- `testing.md` owns row/result quality checks before claiming correctness.
- `safety.md` owns credit spend and downstream mutation boundaries.
- `places.md` owns place/business discovery; `enrichment.md` owns addresses and
  company facts.
