lib/Geo/Address/Parser/Country.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 97.06%
TER3 (LCSAJ): 100.0% (6/6)
Approximate LCSAJ segments: 35

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package Geo::Address::Parser::Country;
    2: 
    3: use strict;
    4: use warnings;
    5: 
    6: use Carp qw(croak carp);
    7: use Locale::Object::Country;
    8: use Object::Configure;
    9: use Params::Get;
   10: use Params::Validate::Strict qw(validate_strict);
   11: use Return::Set qw(set_return);
   12: 
   13: our $VERSION = '0.04';
   14: 
   15: # Direct component-to-country mappings, keyed on lowercase component.
   16: # Values are either a plain country string, or a hashref with
   17: # 'country' and optional 'warning' keys.
   18: my %DIRECT = (
   19: 	'england'                => 'United Kingdom',
   20: 	'scotland'               => 'United Kingdom',
   21: 	'wales'                  => 'United Kingdom',
   22: 	'isle of man'            => 'United Kingdom',
   23: 	'northern ireland'       => 'United Kingdom',
   24: 	'uk'                     => 'United Kingdom',
   25: 	'england, uk'            => 'United Kingdom',
   26: 	'england uk'             => 'United Kingdom',
   27: 
   28: 	# Malformed but seen in real data
   29: 	'scot'                   => {
   30: 		country => 'United Kingdom',
   31: 		warning => "country should be 'Scotland' not 'Scot'",
   32: 	},
   33: 
   34: 	# US variants
   35: 	'usa'                    => 'United States',
   36: 	'us'                     => 'United States',
   37: 	'u.s.a.'                 => 'United States',
   38: 	'united states of america' => 'United States',
   39: 	'united states'          => 'United States',
   40: 
   41: 	# German historical names
   42: 	'preussen'               => 'Germany',
   43: 	"preu\x{00DF}en"         => 'Germany',
   44: 	'deutschland'            => 'Germany',
   45: 
   46: 	# Dutch variants
   47: 	'holland'                => 'Netherlands',
   48: 	'the netherlands'        => 'Netherlands',
   49: 	# 'nl'                   => {	# Not a common abbreviation and clashes with Newfoundland in Canada
   50: 		# country => 'Netherlands',
   51: 		# warning => 'assuming country is Netherlands',
   52: 	# },
   53: 
   54: 	# Slovenian historical name
   55: 	'slovenija'              => 'Slovenia',
   56: 
   57: 	# Canadian provinces/territories missing their country
   58: 	'nova scotia'            => {
   59: 		country => 'Canada',
   60: 		warning => "country 'Canada' missing from record",
   61: 	},
   62: 	'newfoundland'           => {
   63: 		country => 'Canada',
   64: 		warning => "country 'Canada' missing from record",
   65: 	},
   66: 	'nfld'                   => {
   67: 		country => 'Canada',
   68: 		warning => "country 'Canada' missing from record",
   69: 	},
   70: 	'ns'                     => {
   71: 		country => 'Canada',
   72: 		warning => "country 'Canada' missing from record",
   73: 	},
   74: 	'can.'                   => {
   75: 		country => 'Canada',
   76: 		warning => "country 'Canada' missing from record",
   77: 	},
   78: );
   79: 
   80: # Schema for new() arguments, used by Params::Validate::Strict
   81: my $NEW_SCHEMA = {
   82: 	us    => { type => 'object' },
   83: 	ca_en => { type => 'object' },
   84: 	ca_fr => { type => 'object' },
   85: 	au    => { type => 'object' },
   86: 	geonames => {
   87: 		type     => 'object',
   88: 		# can      => 'search',	# Geo::GeoNames is broken, it uses AUTOLOAD.  I must fix that.
   89: 		optional => 1,
   90: 	},
   91: };
   92: 
   93: # Schema for resolve() arguments.
   94: # component is optional: when absent, resolve() extracts it from place
   95: # automatically as the last comma-separated token.
   96: my $RESOLVE_SCHEMA = {
   97: 	place     => { type => 'string', min => 1 },
   98: 	component => { type => 'string', min => 1, optional => 1 },
   99: };
  100: 
  101: # Schema for the resolve() return value
  102: my $RESOLVE_RETURN_SCHEMA = {
  103: 	type   => 'hashref',
  104: 	schema => {
  105: 		country  => { type => 'string',   optional => 1 },
  106: 		place    => { type => 'string',   min => 1 },
  107: 		warnings => { type => 'arrayref' },
  108: 		unknown  => { type => 'boolean' },
  109: 	},
  110: };
  111: 
  112: # Schema for normalise_place() arguments
  113: my $NORMALISE_SCHEMA = {
  114: 	place => { type => 'string', min => 1 },
  115: };
  116: 
  117: # Schema for the normalise_place() return value
  118: my $NORMALISE_RETURN_SCHEMA = {
  119: 	type   => 'hashref',
  120: 	schema => {
  121: 		place    => { type => 'string', min => 1 },
  122: 		warnings => { type => 'arrayref' },
  123: 	},
  124: };
  125: 
  126: =head1 NAME
  127: 
  128: Geo::Address::Parser::Country - Resolve a place string component to a
  129: canonical country name
  130: 
  131: =head1 VERSION
  132: 
  133: Version 0.04
  134: 
  135: =head1 SYNOPSIS
  136: 
  137:     use Geo::Address::Parser::Country;
  138:     use Locale::US;
  139:     use Locale::CA;
  140:     use Locale::AU;
  141: 
  142:     my $resolver = Geo::Address::Parser::Country->new({
  143:         us    => Locale::US->new(),
  144:         ca_en => Locale::CA->new(lang => 'en'),
  145:         ca_fr => Locale::CA->new(lang => 'fr'),
  146:         au    => Locale::AU->new(),
  147:     });
  148: 
  149:     # Simple form: component extracted automatically from place
  150:     my $result = $resolver->resolve(
  151:         place => 'Ramsgate, Kent, England',
  152:     );
  153: 
  154:     # Explicit form: caller supplies the component directly
  155:     my $result = $resolver->resolve(
  156:         component => 'England',
  157:         place     => 'Ramsgate, Kent, England',
  158:     );
  159: 
  160:     # $result->{country}  eq 'United Kingdom'
  161:     # $result->{place}    eq 'Ramsgate, Kent, England'
  162:     # $result->{warnings} is []
  163:     # $result->{unknown}  is 0
  164: 
  165: =head1 DESCRIPTION
  166: 
  167: Resolves the last comma-separated component of a place string into a
  168: canonical country name. Handles common variants, abbreviations, and
  169: historical names found in genealogy data and other poorly-normalised
  170: address sources.
  171: 
  172: Designed specifically to tolerate poor-quality data from software
  173: imports where place strings may be inconsistent, abbreviated, or use
  174: historical country names no longer in common use.
  175: 
  176: Resolution proceeds through the following steps in order:
  177: 
  178: =over 4
  179: 
  180: =item 1. Direct lookup table (covers historical names, abbreviations,
  181: common variants)
  182: 
  183: =item 2. US state code or name via Locale::US
  184: 
  185: =item 3. Canadian province code or name via Locale::CA (English and French)
  186: 
  187: =item 4. Australian state code or name via Locale::AU
  188: 
  189: =item 5. Locale::Object::Country by name
  190: 
  191: =item 6. Geo::GeoNames search (optional, only if object provided at
  192: construction)
  193: 
  194: =item 7. Unknown - returns with C<unknown =E<gt> 1>
  195: 
  196: =back
  197: 
  198: =head1 TODO
  199: 
  200: =over 4
  201: 
  202: =item * Complete C<normalise_place()> to handle missing commas before
  203: country and state names in raw uncleaned input strings. Poor data
  204: import means strings like C<"Houston TX USA"> or
  205: C<"Some Place England"> need comma insertion before component
  206: extraction can work correctly. This should be called before
  207: C<resolve()> for raw uncleaned input.
  208: 
  209: =back
  210: 
  211: =head1 METHODS
  212: 
  213: =head2 new
  214: 
  215: =head3 Purpose
  216: 
  217: Constructs a new resolver object. The locale objects are used for
  218: state and province lookups and are retained for the lifetime of the
  219: object.
  220: 
  221: =head3 API Specification
  222: 
  223: =head4 Input
  224: 
  225:     {
  226:         us    => { type => 'object' },  # Locale::US instance
  227:         ca_en => { type => 'object' },  # Locale::CA English instance
  228:         ca_fr => { type => 'object' },  # Locale::CA French instance
  229:         au    => { type => 'object' },  # Locale::AU instance
  230:         geonames => {                   # Optional Geo::GeoNames instance
  231:             type     => 'object',
  232:             optional => 1,
  233:         },
  234:     }
  235: 
  236: =head4 Output
  237: 
  238:     { type => 'object', isa => 'Geo::Address::Parser::Country' }
  239: 
  240: =head3 Arguments
  241: 
  242: =over 4
  243: 
  244: =item * C<us> - A L<Locale::US> instance. Required.
  245: 
  246: =item * C<ca_en> - A L<Locale::CA> instance with C<lang =E<gt> 'en'>. Required.
  247: 
  248: =item * C<ca_fr> - A L<Locale::CA> instance with C<lang =E<gt> 'fr'>. Required.
  249: 
  250: =item * C<au> - A L<Locale::AU> instance. Required.
  251: 
  252: =item * C<geonames> - An optional L<Geo::GeoNames> instance used as a
  253: last-resort fallback when all other resolution methods fail.
  254: 
  255: =back
  256: 
  257: =head3 Returns
  258: 
  259: A blessed C<Geo::Address::Parser::Country> object.
  260: 
  261: =head3 Side Effects
  262: 
  263: None.
  264: 
  265: =head3 Notes
  266: 
  267: The locale objects are stored by reference and shared for all calls to
  268: C<resolve()>. Constructing them once and reusing the resolver object
  269: is more efficient than constructing a new resolver for each lookup.
  270: 
  271: C<Object::Configure> is used after validation to allow locale objects
  272: to be supplied via environment variables or a config file rather than
  273: always being passed explicitly.
  274: 
  275: =head3 Example
  276: 
  277:     my $resolver = Geo::Address::Parser::Country->new({
  278:         us    => Locale::US->new(),
  279:         ca_en => Locale::CA->new(lang => 'en'),
  280:         ca_fr => Locale::CA->new(lang => 'fr'),
  281:         au    => Locale::AU->new(),
  282:     });
  283: 
  284: =cut
  285: 
  286: sub new {
  287: 	my $class = shift;
  288: 
  289: 	# Accept both hashref and flat list via Params::Get
  290: 	my $args = Params::Get::get_params(undef, @_);
  291: 
  292: 	# Validate all constructor arguments strictly
  293: 	validate_strict({
  294: 		description => 'Geo::Address::Parser::Country::new',
  295: 		input       => $args,
  296: 		schema      => $NEW_SCHEMA,
  297: 	});
  298: 
  299: 	$args = Object::Configure::configure($class, $args);
  300: 
  301: 	# Build and return the blessed object
  302: 	return bless {
  303: 		us       => $args->{us},
  304: 		ca_en    => $args->{ca_en},
  305: 		ca_fr    => $args->{ca_fr},
  306: 		au       => $args->{au},
  307: 		geonames => $args->{geonames},
  308: 	}, $class;
  309: }
  310: 
  311: =head2 resolve
  312: 
  313: =head3 Purpose
  314: 
  315: Resolves the last comma-separated component of a place string to a
  316: canonical country name, and returns the (possibly modified) place
  317: string alongside any warnings generated during resolution.
  318: 
  319: =head3 API Specification
  320: 
  321: =head4 Input
  322: 
  323:     {
  324:         place     => { type => 'string', min => 1 },           # required
  325:         component => { type => 'string', min => 1, optional => 1 },
  326:     }
  327: 
  328: =head4 Output
  329: 
  330:     {
  331:         type   => 'hashref',
  332:         schema => {
  333:             country  => { type => 'string',   optional => 1 },
  334:             place    => { type => 'string',   min => 1 },
  335:             warnings => { type => 'arrayref' },
  336:             unknown  => { type => 'boolean' },
  337:         },
  338:     }
  339: 
  340: =head3 Arguments
  341: 
  342: =over 4
  343: 
  344: =item * C<place> - The full place string, e.g.
  345: C<"Ramsgate, Kent, England">. Required. May be modified by appending a
  346: country suffix where needed.
  347: 
  348: =item * C<component> - The last comma-separated component of the place
  349: string, e.g. C<"England">, C<"TX">, C<"NSW">. Optional. When absent,
  350: C<resolve()> extracts it automatically as the last comma-separated
  351: token of C<place>. When C<place> contains no comma, the entire
  352: C<place> string is used as the component. Supplying C<component>
  353: explicitly is useful when the caller already has it available from a
  354: structured data source.
  355: 
  356: =back
  357: 
  358: =head3 Returns
  359: 
  360: A hashref containing:
  361: 
  362: =over 4
  363: 
  364: =item * C<country> - The canonical country name as a string, e.g.
  365: C<"United Kingdom">. C<undef> if resolution failed.
  366: 
  367: =item * C<place> - The full place string, possibly with a country
  368: suffix appended (e.g. C<", USA">). Always returned even if unmodified.
  369: 
  370: =item * C<warnings> - An arrayref of warning strings generated during
  371: resolution. May be empty. The caller is responsible for acting on
  372: these, e.g. by passing them to a C<complain()> function.
  373: 
  374: =item * C<unknown> - A boolean. True if the country could not be
  375: resolved by any method.
  376: 
  377: =back
  378: 
  379: =head3 Side Effects
  380: 
  381: None. All warnings are returned to the caller rather than emitted
  382: directly.
  383: 
  384: =head3 Notes
  385: 
  386: Resolution order is: direct lookup, US state, Canadian province,
  387: Australian state, Locale::Object::Country, GeoNames (if available).
  388: The first successful match wins.
  389: 
  390: When a US state, Canadian province, or Australian state is recognised,
  391: the appropriate country string (C<", USA">, C<", Canada">,
  392: C<", Australia">) is appended to C<place> if not already present.
  393: 
  394: =head3 Example
  395: 
  396:     # Simple form - component extracted automatically
  397:     my $result = $resolver->resolve(
  398:         place => 'Houston, TX',
  399:     );
  400: 
  401:     # Explicit form - component supplied by caller
  402:     my $result = $resolver->resolve(
  403:         component => 'TX',
  404:         place     => 'Houston, TX',
  405:     );
  406: 
  407:     # $result->{country}     eq 'United States'
  408:     # $result->{place}       eq 'Houston, TX, USA'
  409:     # $result->{warnings}[0] eq 'TX: assuming country is United States'
  410:     # $result->{unknown}     is 0
  411: 
  412: =cut
  413: 
  414: sub resolve {
415 → 447 → 516415 → 447 → 0  415: 	my $self = shift;
  416: 
  417: 	# Accept both hashref and flat list
  418: 	my $args = Params::Get::get_params(undef, @_);
  419: 
  420: 	# Validate input arguments
  421: 	validate_strict({
  422: 		description => 'Geo::Address::Parser::Country::resolve',
  423: 		input       => $args,
  424: 		schema      => $RESOLVE_SCHEMA,
  425: 	});
  426: 
  427: 	my $place = $args->{place};
  428: 
  429: 	# Extract the component from the place string when the caller has not
  430: 	# supplied it explicitly.  The component is the last comma-separated
  431: 	# token, trimmed of surrounding whitespace.  When the place string
  432: 	# contains no comma at all, the entire string is the component.
  433: 	my $component = $args->{component}
  434: 		// do {
  435: 			my ($c) = $place =~ /(?:^|,)\s*([^,]+?)\s*$/;
  436: 			$c // $place;
  437: 		};
  438: 
  439: 	my $lc = lc($component);
  440: 
  441: 	# Accumulate warnings to return to caller rather than emitting them
  442: 	my @warnings;
  443: 	my $country;
  444: 
  445: 	# Step 1: check the direct lookup table first, handles historical
  446: 	# names, common abbreviations and malformed but real-world input
  447: 	if(my $match = $DIRECT{$lc}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

448: if(ref($match) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

449: $country = $match->{country}; 450: push @warnings, $match->{warning} if $match->{warning}; 451: } else { 452: $country = $match; 453: } 454: } 455: 456: # Step 2: two-letter US state code, e.g. "TX" 457: elsif($component =~ /^[A-Z]{2}$/i 458: && $self->{us}{code2state}{uc($component)}) { 459: $country = 'United States'; 460: push @warnings, "$component: assuming country is United States"; 461: $place = $self->_append_country($place, 'USA'); 462: } 463: 464: # Step 3: US state full name, e.g. "Texas" 465: elsif($self->{us}{state2code}{uc($component)}) { 466: $country = 'United States'; 467: push @warnings, "$component: assuming country is United States"; 468: $place = $self->_append_country($place, 'USA'); 469: } 470: 471: # Step 4: Canadian province code in English or French 472: elsif($component =~ /^[A-Z]{2}$/i 473: && ($self->{ca_en}{code2province}{uc($component)} 474: || $self->{ca_fr}{code2province}{uc($component)})) { 475: $country = 'Canada'; 476: push @warnings, "$component: assuming country is Canada"; 477: $place = $self->_append_country($place, 'Canada'); 478: } 479: 480: # Step 5: Canadian province full name in English or French 481: elsif($self->{ca_en}{province2code}{uc($component)} 482: || $self->{ca_fr}{province2code}{uc($component)}) { 483: $country = 'Canada'; 484: push @warnings, "$component: assuming country is Canada"; 485: $place = $self->_append_country($place, 'Canada'); 486: } 487: 488: # Step 6: Australian state code, e.g. "NSW", "VIC" 489: elsif($component =~ /^[A-Z]{2,3}$/i 490: && $self->{au}{code2state}{$component}) { 491: $country = 'Australia'; 492: push @warnings, "$component: assuming country is Australia"; 493: $place = $self->_append_country($place, 'Australia'); 494: } 495: 496: # Step 7: Australian state full name 497: elsif($self->{au}{state2code}{uc($component)}) { 498: $country = 'Australia'; 499: push @warnings, "$component: assuming country is Australia"; 500: $place = $self->_append_country($place, 'Australia'); 501: } 502: 503: # Step 8: fall back to Locale::Object::Country by name 504: elsif(my $loc = Locale::Object::Country->new(name => $component)) { 505: $country = $loc->name(); 506: } 507: 508: # Step 9: optional GeoNames fallback for anything still unresolved 509: elsif($self->{geonames}) { 510: $country = $self->_geonames_lookup( 511: $place, $component, \@warnings 512: ); 513: } 514: 515: # Build and validate the return structure before handing back 516 → 523 → 0 516: my $result = { 517: country => $country, 518: place => $place, 519: warnings => \@warnings, 520: unknown => defined($country) ? 0 : 1, 521: }; 522: 523: return set_return($result, $RESOLVE_RETURN_SCHEMA); 524: } 525: 526: =head2 normalise_place 527: 528: =head3 Purpose 529: 530: Inserts missing commas into a raw, uncleaned place string so that 531: C<resolve()> can reliably extract the last component. Raw input from 532: poor-quality data imports frequently omits the commas that separate 533: city, state, and country tokens. 534: 535: =head3 API Specification 536: 537: =head4 Input 538: 539: { 540: place => { type => 'string', min => 1 }, 541: } 542: 543: =head4 Output 544: 545: { 546: type => 'hashref', 547: schema => { 548: place => { type => 'string', min => 1 }, 549: warnings => { type => 'arrayref' }, 550: }, 551: } 552: 553: =head3 Arguments 554: 555: =over 4 556: 557: =item * C<place> - The raw place string to normalise, e.g. 558: C<"Houston TX USA"> or C<"Some Place England">. Required. 559: 560: =back 561: 562: =head3 Returns 563: 564: A hashref containing: 565: 566: =over 4 567: 568: =item * C<place> - The normalised place string with commas inserted 569: where they were missing, e.g. C<"Houston, TX, USA">. Always returned 570: even if no changes were made. 571: 572: =item * C<warnings> - An arrayref of warning strings generated during 573: normalisation, e.g. noting where commas were inserted. May be empty. 574: 575: =back 576: 577: =head3 Side Effects 578: 579: None. 580: 581: =head3 Notes 582: 583: This method is not yet fully implemented. It currently returns the 584: place string unchanged. Implementation requires scanning the token 585: sequence against the locale tables (US states, Canadian provinces, 586: Australian states, and the %DIRECT country table) to identify where 587: comma boundaries belong. 588: 589: Call this method before C<resolve()> when working with raw input that 590: may lack commas: 591: 592: my $norm = $resolver->normalise_place(place => 'Houston TX USA'); 593: my $result = $resolver->resolve(place => $norm->{place}); 594: 595: =head3 Example 596: 597: my $norm = $resolver->normalise_place(place => 'Some Place England'); 598: # $norm->{place} eq 'Some Place, England' (once implemented) 599: # $norm->{warnings} contains a note about comma insertion 600: 601: =cut 602: 603: sub normalise_place { 604: my $self = shift; 605: 606: # Accept both hashref and flat list 607: my $args = Params::Get::get_params(undef, @_); 608: 609: # Validate input arguments 610: validate_strict({ 611: description => 'Geo::Address::Parser::Country::normalise_place', 612: input => $args, 613: schema => $NORMALISE_SCHEMA, 614: }); 615: 616: my $place = $args->{place}; 617: my @warnings; 618: 619: # TODO: scan tokens against locale tables and %DIRECT, inserting 620: # commas where boundaries are identified. For example: 621: # 'Houston TX USA' -> 'Houston, TX, USA' 622: # 'Some Place England' -> 'Some Place, England' 623: # This requires iterating right-to-left through whitespace-separated 624: # tokens, matching each against known country/state/province names, 625: # and inserting a comma immediately before the first match found. 626: 627: my $result = { 628: place => $place, 629: warnings => \@warnings, 630: }; 631: 632: return set_return($result, $NORMALISE_RETURN_SCHEMA); 633: } 634: 635: # _append_country 636: # 637: # Purpose: 638: # Appends a country suffix to a place string if not already present. 639: # 640: # Entry criteria: 641: # $self - blessed object 642: # $place - non-empty place string 643: # $suffix - country string to append, e.g. 'USA' 644: # 645: # Exit status: 646: # Returns the (possibly modified) place string. 647: # 648: # Side effects: 649: # None. 650: # 651: # Notes: 652: # Uses a case-insensitive check to avoid double-appending. 653: # E.g. 'Houston, TX' becomes 'Houston, TX, USA'. 654: # 'Houston, TX, USA' is returned unchanged. 655: 656: sub _append_country { 657: my ($self, $place, $suffix) = @_; 658: 659: # Return unchanged if suffix already present at end of string 660: return $place if $place =~ /,\s*\Q$suffix\E\s*$/i;

Mutants (Total: 2, Killed: 2, Survived: 0)

661: 662: return "$place, $suffix";

Mutants (Total: 2, Killed: 2, Survived: 0)

663: } 664: 665: # _geonames_lookup 666: # 667: # Purpose: 668: # Uses the optional Geo::GeoNames object to search for a country 669: # name when all other resolution methods have failed. 670: # 671: # Entry criteria: 672: # $self - blessed object with {geonames} set 673: # $place - full place string, used as the search query for 674: # maximum context 675: # $component - original component string, used in warning text 676: # $warnings - arrayref to push warnings onto 677: # 678: # Exit status: 679: # Returns the country name string on success, undef on failure. 680: # 681: # Side effects: 682: # Pushes a warning onto $warnings if a country is found via GeoNames. 683: # 684: # Notes: 685: # We search on the full place string rather than just the component 686: # to give GeoNames maximum context and improve result accuracy. 687: # The first result is used. 688: 689: sub _geonames_lookup { 690 → 699 → 706690 → 699 → 0 690: my ($self, $place, $component, $warnings) = @_; 691: 692: my $result = $self->{geonames}->search( 693: q => $place, 694: style => 'FULL', 695: ); 696: 697: # Normalise to the first element if an arrayref was returned. 698: # Guard against a bare scalar (e.g. an error string) being returned. 699: if(ref($result) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

700: $result = $result->[0]; 701: } elsif(!ref($result)) { 702: return; # scalar return is not a hashref — bail out cleanly 703: } 704: 705: # Must be a plain hashref at this point 706 → 715 → 0 706: return unless ref($result) eq 'HASH'; 707: 708: # Extract country name; must be a defined, non-ref, non-empty string 709: my $country = $result->{countryName}; 710: return unless defined($country) 711: && !ref($country) 712: && length($country); 713: 714: push @{$warnings}, "$component: assuming country is $country"; 715: return $country;

Mutants (Total: 2, Killed: 2, Survived: 0)

716: } 717: 718: =head1 AUTHOR 719: 720: Nigel Horne C<< <njh@nigelhorne.com> >> 721: 722: =head1 REPOSITORY 723: 724: L<https://github.com/nigelhorne/Geo-Address-Parser-Country> 725: 726: =head1 SUPPORT 727: 728: This module is provided as-is without any warranty. 729: 730: Please report any bugs or feature requests to C<bug-geo-address-parser at rt.cpan.org>, 731: or through the web interface at 732: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Address-Parser-Country>. 733: I will be notified, and then you'll 734: automatically be notified of progress on your bug as I make changes. 735: 736: =head1 BUGS 737: 738: =over 4 739: 740: =item * C<normalise_place()> is not yet implemented. It currently 741: returns the place string unchanged. See L</normalise_place> for 742: details of the planned behaviour. 743: 744: =item * The step 6 Australian state code lookup uses the raw, 745: un-normalised component as the hash key, making it case-sensitive 746: unlike steps 2-5. Lowercase codes such as C<nsw> will not match. 747: A fix to apply C<uc($component)> consistently is pending. 748: 749: =item * C<Geo::GeoNames> generates its query methods via C<AUTOLOAD>, 750: so C<can('search')> returns false at the Perl level even though 751: C<$geonames-E<gt>search(...)> works correctly at runtime. The 752: C<can =E<gt> 'search'> schema check has been commented out as a 753: temporary workaround pending a fix to C<Geo::GeoNames> itself. 754: 755: =back 756: 757: Please report additional bugs via the GitHub issue tracker: 758: L<https://github.com/nigelhorne/Geo-Address-Parser-Country/issues> 759: 760: =head1 SEE ALSO 761: 762: =over 4 763: 764: =item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Address-Parser-Country/coverage/> 765: 766: =item * L<Geo::Address::Parser> 767: 768: =item * L<Locale::US> 769: 770: =item * L<Locale::CA> 771: 772: =item * L<Locale::AU> 773: 774: =item * L<Locale::Object::Country> 775: 776: =item * L<Geo::GeoNames> 777: 778: =item * L<Object::Configure> 779: 780: =item * L<Params::Get> 781: 782: =item * L<Params::Validate::Strict> 783: 784: =item * L<Return::Set> 785: 786: =back 787: 788: =head1 LICENCE AND COPYRIGHT 789: 790: Copyright 2026 Nigel Horne. 791: 792: Usage is subject to GPL2 licence terms. 793: If you use it, please let me know. 794: 795: =cut 796: 797: 1;