NAME

Geo::Coder::Free::Local - Geocode using user-curated local data

VERSION

Version 0.42

SYNOPSIS

use Geo::Coder::Free::Local;

my $geocoder = Geo::Coder::Free::Local->new();
my $location = $geocoder->geocode(location => 'Ramsgate, Kent, UK');
printf "lat=%.6f lon=%.6f\n", $location->lat(), $location->long();

DESCRIPTION

Provides geocoding via a user-curated CSV dataset embedded in the module's __DATA__ section. Locations in the data were verified by GPS and by inspecting geotagged photographs. The data is read once at construction time and indexed for fast lookup.

This is the highest-priority backend tried by Geo::Coder::Free.

LIMITATIONS

  • The embedded __DATA__ dataset covers only a small set of hand-picked locations. There is no mechanism for non-authors to contribute data without patching the module.

  • Canadian and Australian address parsing is not yet implemented; those queries return undef.

  • _search performs an O(n) linear scan through all rows. The hash-based index in new() handles exact string matches; no index exists for partial field matches.

  • our %alternatives duplicates mappings in Geo::Coder::Free::__DATA__. Both should be consolidated into a shared external config file.

  • $libpostal_is_installed is a module-level (effectively global) flag. Not thread-safe in a forking or threaded Perl deployment.

  • The __DATA__ filehandle is a one-shot resource. The first call to new() exhausts it with my @data = <DATA>; every subsequent new() in the same process reads zero rows and builds an empty index. Construct exactly one Local object per process and share it.

  • The hash-index key is lc(Geo::Location::Point->new($row)->as_string()), which includes the name field. To get a direct index hit the caller must supply the full string with the venue/place name as the leading component; an address that omits the name falls through to the slower O(n) _search path.

METHODS

new

SYNOPSIS

my $geocoder = Geo::Coder::Free::Local->new();
my $geocoder = Geo::Coder::Free::Local->new(cache => $chi_cache);

DESCRIPTION

Constructor. Reads the __DATA__ CSV block, builds a hash-based lookup index, and derives the geographic centre of any city/state/country cluster containing three or more data points.

One-shot filehandle: the DATA handle is consumed on the first call to new(). Any subsequent call to new() returns an object whose dataset and index are both empty. In code that constructs multiple Local objects (e.g. in tests) construct exactly one instance and reuse it across all callers.

API SPECIFICATION

input

# Input schema (Params::Validate::Strict)
cache => { type => 'object', optional => 1, can => ['get', 'set'] }  # CHI-compatible cache object

output

# Output schema (Return::Set)
{ type => 'object', isa => 'Geo::Coder::Free::Local' }

FORMAL SPECIFICATION

LocalState ::= ⟨⟨ data  : seq Row;
                   index : Map[STRING → Row];
                   cache : Map[STRING → Point] ⟩⟩

Init : Params → LocalState
∀ p : Params •
  let rows    == parse_csv(__DATA__) ∪ geographic_centres(__DATA__) •
  let idx_key == λ r • lc(Point(r).as_string()) •
  LocalState.index = { idx_key(r) ↦ r | r ∈ rows }

geocode

SYNOPSIS

my $pt = $geocoder->geocode(location => '203 E Chatsworth Rd, Reisterstown, Baltimore, MD, US');
print $pt->lat(), "\n";

# All calling forms are accepted:
$geocoder->geocode('203 E Chatsworth Rd, Reisterstown, MD, US');
$geocoder->geocode({ location => '203 E Chatsworth Rd, Reisterstown, MD, US' });

API SPECIFICATION

input

# Input schema (Params::Validate::Strict)
location => { type => 'scalar' }  # address string; must contain at least two commas

output

# Output schema (Return::Set)
{ type => 'object', isa => 'Geo::Location::Point', optional => 1 }

MESSAGES

Usage: ...::geocode(...)   No location argument given.

FORMAL SPECIFICATION

Geocode : STRING → Point?
∀ addr : STRING •
  let norm == lc(replace_usa(addr)) •
  (norm ∈ cache ⟹ result = cache[norm]) ∧
  (norm ∈ index ⟹ result = index[norm]) ∧
  (¬ result ⟹ result = parse_and_search(addr))

PSEUDOCODE

normalise @_ into %params
reject if location does not contain two or more commas (not a full address)
check cache; return hit
check hash index; return hit and cache it
# index key includes the 'name' field; supply the venue/place name as the
# leading address component to guarantee an index hit
attempt country-specific parser (US, GB; skip CA/AU pending implementation)
attempt Geo::StreetAddress::US for "..., USA" addresses
attempt Geo::Address::Parser
attempt Geo::libpostal (large memory footprint; loaded lazily)
attempt 4-part regex decomposition (name/road/city/state/country)
attempt %alternatives mapping
return undef

reverse_geocode

SYNOPSIS

my $loc = $geocoder->reverse_geocode(latlng => '51.3341,-1.4159');
# Returns the location string(s) for that lat/lon pair.

API SPECIFICATION

input

# Input schema (Params::Validate::Strict) — latlng or lat+lon required
latlng => { type => 'scalar', optional => 1 }  # "$lat,$long" comma-separated decimal degrees
lat    => { type => 'scalar', optional => 1 }  # latitude  (alternative to latlng)
lon    => { type => 'scalar', optional => 1 }  # longitude (alternative to latlng)
long   => { type => 'scalar', optional => 1 }  # alias for lon

output

# Output schema (Return::Set)
# scalar context: { type => 'scalar',   optional => 1 }  # location string
# list context:   { type => 'arrayref', of => { type => 'scalar' } }

ua

Does nothing — present for drop-in compatibility with other Geo::Coder::* modules.

AUTHOR

Nigel Horne <njh@nigelhorne.com>

BUGS

The data are stored in the module source and must be maintained by the author. A future version should load them from an external file to allow community contributions.

See also: https://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Coder-Free

SEE ALSO

Geo::Coder::Free, Geo::Coder::Free::OpenAddresses, Geo::Coder::Free::MaxMind

LICENSE AND COPYRIGHT

Copyright 2020-2026 Nigel Horne.

The program code is released under the following licence: GPL2 for personal use on a single computer. All other users (including Commercial, Charity, Educational, and Government) must apply in writing for a licence for use from Nigel Horne at <njh at nigelhorne.com>.