NAME
Geo::Coder::Free::Local - Geocode using user-curated local data
VERSION
Version 0.47
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._searchperforms an O(n) linear scan through all rows. The hash-based index innew()handles exact string matches; no index exists for partial field matches.our %alternativesduplicates mappings inGeo::Coder::Free::__DATA__. Both should be consolidated into a shared external config file.$libpostal_is_installedis 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 tonew()exhausts it withmy @data = <DATA>; every subsequentnew()in the same process reads zero rows and builds an empty index. Construct exactly oneLocalobject per process and share it.The hash-index key is
lc(Geo::Location::Point->new($row)->as_string()), which includes thenamefield. 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)_searchpath.
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 }
PROVENANCE
Every result carries geocoder, database, confidence and accuracy (section 2.1 of BI-Phase6-Dependency-Specs.md). This backend reports database as Geo::Coder::Free::Local, and accuracy is what the matched columns justify: a house number makes it an address, a road a street, a name or city a locality, a state a region and a country a country. A lookup answered from the index, which matches a whole row, is an exact match (confidence of 1.0) whose accuracy comes from the row's own fields - so a derived geographic centre, which carries a city, state and country and nothing narrower, is a locality.
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.
_result_for
Build the result for a dataset row that the index matched. The index holds the rows themselves - the same hashrefs _search scans - so the object is built as a copy rather than blessed in place, and the provenance keys every result carries (section 2.1 of BI-Phase6-Dependency-Specs.md) are added where the row does not already record them.
An index hit is an exact match on the row's string, so the confidence is $CONF_EXACT, and the accuracy is what the row's own fields justify, through _accuracy_for().
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>.