# `GeoGenius`
[🔗](https://github.com/agoodway/geo_genius/blob/main/lib/geo_genius.ex#L1)

A versioned catalog of named geographic areas for Elixir and
PostgreSQL/PostGIS applications.

# `ancestors_of`

```elixir
@spec ancestors_of(String.t(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas above this one. Accepts `:classifications` and `:max_depth`.

# `ancestors_of_many`

```elixir
@spec ancestors_of_many(GeoGenius.Store.seeds(), keyword()) :: [
  GeoGenius.SeededMatch.t()
]
```

Areas above each of these, one call instead of one call per key. Seed
attribution and options match `children_of_many/2`.

# `areas_by_code`

```elixir
@spec areas_by_code(String.t(), String.t(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas carrying a code.

A code is unique only within a parent, so `:parent_area_key` scopes the
lookup and `:parent_max_depth` controls how far down it reaches. Always
returns a list: several areas can legitimately share one code.

# `areas_by_code_many`

```elixir
@spec areas_by_code_many(String.t(), GeoGenius.Store.seeds(), keyword()) :: [
  GeoGenius.SeededMatch.t()
]
```

Areas carrying any of these code values under one code type, one call
instead of one call per value.

The seed here is the code value, not an area key, so `:seed_key` holds the
value that found each row. Options match `areas_by_code/3`, including
`:parent_area_key` and `:parent_max_depth`, which apply to every value in
the list.

# `areas_for_geometry`

```elixir
@spec areas_for_geometry(Geo.geometry(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas overlapping a geometry, with the measured overlap of each.

# `areas_for_point`

```elixir
@spec areas_for_point(GeoGenius.Store.numeric(), GeoGenius.Store.numeric(), keyword()) ::
  [
    GeoGenius.AreaMatch.t()
  ]
```

Areas whose boundary covers a point.

Longitude first. Coordinates may be floats, integers, or the `%Decimal{}`
a `numeric` column loads as. Options: `:collections`, `:types`,
`:release_id`, `:include_retired`, plus `:repo` and `:prefix`.

# `areas_near`

```elixir
@spec areas_near(
  GeoGenius.Store.numeric(),
  GeoGenius.Store.numeric(),
  GeoGenius.Store.numeric(),
  keyword()
) :: [GeoGenius.AreaMatch.t()]
```

Areas within a radius of a point, nearest first. Accepts `:limit`, `nil` for none.

# `artifact_path`

```elixir
@spec artifact_path(String.t(), String.t(), keyword()) ::
  {:ok, Path.t()} | {:error, GeoGenius.ArtifactError.t()}
```

The local file one artifact of a release resolves to.

Returns `{:error, %GeoGenius.ArtifactError{reason: :not_cached}}` naming both
the expected path and the cache key when nothing is there yet, rather than a
path to a file that does not exist.

# `await`

```elixir
@spec await(Ecto.UUID.t(), timeout() | nil, keyword()) ::
  {:ok, GeoGenius.ImportRun.t()}
  | {:error, GeoGenius.ImportRun.t()}
  | {:error, :timeout}
```

Polls the catalog for a run's outcome, in the calling process.

Returns `{:ok, run}` once `GeoGenius.ImportRun.finished?/1` is true and the
run succeeded, `{:error, run}` once it finished and failed, and
`{:error, :timeout}` once `timeout` elapses with neither. Polls the catalog
every 250ms rather than monitoring a process: the run may be executing on
another node entirely, and the durable row in PostgreSQL is the only thing
both sides can see.

`timeout` resolves in this order: the argument given here, then
`config :geo_genius, :await_timeout`, then the library default of
1,800,000ms (thirty minutes) -- comfortably past the ~17 minutes a full US
SimpleMaps import takes, without blocking a caller that never says
otherwise for the length of a genuinely hung run. `:infinity` is accepted
at every level for a caller willing to wait as long as it takes.

# `children_of`

```elixir
@spec children_of(String.t(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas below this one. Accepts `:classifications` and `:max_depth`.

# `children_of_many`

```elixir
@spec children_of_many(GeoGenius.Store.seeds(), keyword()) :: [
  GeoGenius.SeededMatch.t()
]
```

Areas below each of these, one call instead of one call per key.

Every row carries the seed it came from in `GeoGenius.SeededMatch`'s
`:seed_key`, because a result mixes rows from every seed. A seed with no
children contributes no row rather than an empty one, and the rows come back
in the order the seeds were given. Takes the same options as
`children_of/2`.

Resolving a list this way collapses N round trips to one, which is the whole
of what it saves: the SQL still runs the singular read once per seed, and
the planner cannot push a predicate into a `SETOF` function either way. A
caller that wants to join catalog areas against its own tables, or aggregate
over them, wants `GeoGenius.Query`'s composable path instead of this.

    GeoGenius.children_of_many(county_keys)
    |> Enum.group_by(& &1.seed_key, & &1.match)

# `import`

```elixir
@spec import(keyword()) :: {:ok, Ecto.UUID.t()} | {:error, term()}
```

Atomically registers an exact release candidate, claims its import run, and
enqueues it. Never runs the import itself -- `runner.enqueue/3` starts that,
and `status/2` or `await/3` reads it back.

`opts[:manifest]` is used as given; otherwise the manifest is loaded via
`GeoGenius.Manifest.load/3` from `opts[:collection]` and `opts[:release]`
(both required in that case). A completed unpublished candidate returns its
existing run without enqueueing, while the owner of a live run may safely
submit that same run to its runner again.

Repeating an import whose latest attempt failed returns
`{:error, %GeoGenius.CandidateError{reason: :failed}}` with that attempt's
run id. It never creates a replacement implicitly; pass the failed run id
to `retry_failed/2` to create the next attempt.

`:owner` defaults to `to_string(node())`. Repeating the exact request under
the same owner closes a post-commit enqueue gap by delivering the existing
run again; the run's executor claim makes a duplicate delivery a no-op, not
a takeover. A caller running two importers on one node passes `:owner`
explicitly. `:stale_after_seconds` (default 900) is the window used to
diagnose an abandoned run; staleness never transfers its executor or
creates a retry. The value also travels through the runner's `args` so
`GeoGenius.Pipeline` can derive its statement timeout from the original
claim. `:publish` (default `false`) is forwarded the same way.

Returns `{:error, exception}` -- never raises -- for a manifest that
cannot be resolved (`GeoGenius.ManifestError`) or a catalog write that
cannot be completed (`GeoGenius.CatalogError`). Expected candidate
refusals, including a published release, return
`GeoGenius.CandidateError` with a closed `:reason` and the conflicting
release/run identifiers. A runner enqueue failure returns
`GeoGenius.EnqueueError`, including the run id, backend, acceptance
certainty, original reason, and safe status/redelivery guidance.

# `publish`

```elixir
@spec publish(Ecto.UUID.t(), keyword()) :: {:ok, Ecto.UUID.t()} | {:error, term()}
```

Publishes a verified release, returning its id.

The SQL `publish_release` re-runs the release's verification inside the
publication, so this is one of the library's long single statements.
`:timeout` bounds it, in milliseconds, and defaults to 900,000 -- the window
`GeoGenius.Pipeline` gives the same statement in its verifying phase --
rather than DBConnection's fifteen-second default, which a national release
exceeds by orders of magnitude.

Returns `{:error, exception}` -- never raises -- when the release fails
verification or the catalog write cannot be completed.

Only the publication is guarded. The collection key travelling in the
`:release_published` payload is read back after the publication has already
committed, and a read that fails there costs the payload its
`:collection_key`, never the outcome: a publication that committed is
reported as `{:ok, release_id}` and the event still fires.

# `published_release`

```elixir
@spec published_release(String.t(), keyword()) :: Ecto.UUID.t() | nil
```

The currently published release for a collection, or nil if none is published.

# `related_areas`

```elixir
@spec related_areas(String.t(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas related to this one in either direction, one level out.

# `related_areas_many`

```elixir
@spec related_areas_many(GeoGenius.Store.seeds(), keyword()) :: [
  GeoGenius.SeededMatch.t()
]
```

Areas related to each of these in either direction, one call instead of one
call per key. Seed attribution and options match `children_of_many/2`.

# `release_artifacts`

```elixir
@spec release_artifacts(String.t(), keyword()) ::
  {:ok, [GeoGenius.ReleaseArtifacts.Artifact.t()]}
  | {:error, GeoGenius.ArtifactError.t()}
```

Every artifact the collection's published release was built from, with where
each one resolves to on this machine, ordered by logical name.

`:release_id` reads a release other than the published one, which is how a
host fills a projection for a release before it goes live. See
`GeoGenius.ReleaseArtifacts` and `guides/projections.md`.

# `release_at`

```elixir
@spec release_at(DateTime.t(), keyword()) :: Ecto.UUID.t() | nil
```

The release a collection had published at a moment, or nil if it had
published nothing yet. Requires `:collection`.

Reads take a release id rather than a timestamp, so this is how a caller
holding a timestamp gets an argument for `:release_id`.

# `resolve`

```elixir
@spec resolve(map(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Resolves a mixed input through the cascade, returning the first strategy that
matched.

`input` carries whatever the caller holds: `lon` and `lat` together,
`code_type` and `code_value` together, `name`, and `radius_m` for the
proximity strategy. `parent_area_key` is not a signal of its own. It scopes
the code and name strategies to one area's descendants; containment and
proximity ignore it, holding a coordinate already.

Accepts `:strategies` to constrain or reorder the cascade.

# `retry_failed`

```elixir
@spec retry_failed(Ecto.UUID.t(), keyword()) ::
  {:ok, Ecto.UUID.t()} | {:error, term()}
```

Explicitly replaces a failed latest attempt with the supplied exact
manifest and enqueues the newly claimed attempt.

The manifest and runner options resolve exactly as they do for `import/1`.
Candidate mismatches, protected releases, non-latest attempts, and runs that
are not failed return a structured `GeoGenius.CandidateError` without
enqueueing anything.

# `rollback`

```elixir
@spec rollback(String.t(), keyword()) :: {:ok, Ecto.UUID.t()} | {:error, term()}
```

Rolls a collection's publication back to its previous release, returning
that release's id.

Returns `{:error, exception}` -- never raises, and never clears the
publication -- for a collection with no previous release to roll back to,
and `{:error, String.t()}` for a collection key the catalog does not carry.

Only the rollback is guarded. The release the collection now publishes is
read back afterwards, and a read that fails there returns
`{:error, {:unread, message}}`, a shape distinct from every failure shape
above: the publication moved whether or not the id can be read, so a caller
matching this clause knows the rollback happened and must not retry it. The
message says the same thing in words. `GeoGenius.Pipeline` uses
`{:error, {:unrecorded, message}}` for its own committed-but-unread outcome;
this is that pattern at the public API. The `:release_rolled_back` event
fires either way, carrying a `nil` `:release_id` when the read failed.

# `search_areas`

```elixir
@spec search_areas(String.t(), keyword()) :: [GeoGenius.AreaMatch.t()]
```

Areas ranked by name similarity. Accepts `:limit`, `nil` for none.

# `status`

```elixir
@spec status(Ecto.UUID.t(), keyword()) :: GeoGenius.ImportRun.t() | nil
```

One import run, or nil when no run carries that id.

# `verify`

```elixir
@spec verify(module(), keyword()) :: :ok | {:error, [String.t()]}
```

Verifies that the database satisfies GeoGenius's prerequisites.

Returns `:ok`, or `{:error, reasons}` naming every unmet prerequisite.

# `verify!`

```elixir
@spec verify!(module(), keyword()) :: :ok
```

Verifies prerequisites and raises `GeoGenius.PreflightError` when any are unmet.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
