GeoGenius (geo_genius v0.2.0)

Copy Markdown View Source

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

Summary

Functions

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

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

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

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

Areas whose boundary covers a point.

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

The local file one artifact of a release resolves to.

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

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

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

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.

Publishes a verified release, returning its id.

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

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

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.

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

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

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

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

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

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

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

Verifies that the database satisfies GeoGenius's prerequisites.

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

Functions

ancestors_of(area_key, opts \\ [])

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

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

ancestors_of_many(area_keys, opts \\ [])

@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(code_type, code_value, opts \\ [])

@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(code_type, code_values, opts \\ [])

@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(geometry, opts \\ [])

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

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

areas_for_point(lon, lat, opts \\ [])

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(lon, lat, radius_m, opts \\ [])

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

artifact_path(collection_key, logical_name, opts \\ [])

@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(run_id, timeout \\ nil, opts \\ [])

@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(area_key, opts \\ [])

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

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

children_of_many(area_keys, opts \\ [])

@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(opts \\ [])

@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(release_id, opts \\ [])

@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(collection_key, opts \\ [])

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

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

release_artifacts(collection_key, opts \\ [])

@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(as_of, opts \\ [])

@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(input, opts \\ [])

@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(failed_run_id, opts \\ [])

@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(collection_key, opts \\ [])

@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(query, opts \\ [])

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

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

status(run_id, opts \\ [])

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

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

verify(repo, opts \\ [])

@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!(repo, opts \\ [])

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

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