NAME
Geo::Coder::Free::Local - Geocode using user-curated local data
VERSION
Version 0.52
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 }
reads
my $count = $geocoder->reads();
How many times this backend has searched its dataset since the object was created. A lookup answered from the index, from the result cache or from the shared miss cache does not increase it, so geocode_many() can take the count before and after a value to tell a lookup that cost a search from one that was answered from a cache.
API SPECIFICATION
input
# Input schema (Params::Validate::Strict)
# none
output
# Output schema (Return::Set)
{ type => 'integer', min => 0 } # 0 for a new object
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
The domain of location:
A whole row of the data matches in any case, and
USAis the same asUS.Fewer than two commas, or two commas with nothing between them (
'Road,,GB'), finds nothing and searches nothing. Two commas with one character between them ('Road, ,GB') is enough to go on to the search.Any characters are accepted, and an address that isn't found is remembered as a miss. Undecoded bytes are a different key from the characters they encode.
undef,''and'0', an empty hashref or no arguments croakUsage: geocode(location => $location). A reference other than a hashref, or an odd list longer than one, croaksUsage: ...::geocode(...).
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
The domain of the coordinates:
Each is compared to the data at four significant figures, not four decimal places:
39.4749and39.47both match a row at39.46727, and39.4751does not.No range check applies.
'0,0'and'91,181'find nothing and are not errors. A third part is dropped.Each must be an optional minus, ASCII digits, then an optional point and more digits, as in "reverse_geocode" in Geo::Coder::Free::MaxMind. Anything else croaks
Invalid coordinate value for reverse_geocode.latlngis used when it is true. No arguments,''or'0', one part with no comma, or only one oflatandlon, croakUsage:. So do a reference other than a hashref and an odd list longer than one.Nothing found is
0in scalar context and an empty list in list context.
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.
API SPECIFICATION
input
# Input schema (Params::Validate::Strict)
# any arguments are accepted and ignored
output
# Output schema (Return::Set)
# nothing: an empty list, or undef in scalar context
_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 Geo::Location::Point::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>.