mainegeo.matching
This module generates or loads a database of Maine town names and their aliases, and provides a function for matching.
1"""This module generates or loads a database of Maine town names and their 2aliases, and provides a function for matching. 3""" 4 5__docformat__ = 'google' 6 7__all__ = [ 8 'TownReference', 9 'TownDatabase', 10 'get_town_database' 11] 12 13from dataclasses import dataclass 14from functools import cached_property, cache 15from typing import List, Dict, Optional, ClassVar 16from pathlib import Path 17from ruamel.yaml import YAML 18from mainegeo.paths import TOWNSHIPS_JSON, TOWNSHIPS_YAML 19from mainegeo.entities import (County, Cousub, TownType) 20from mainegeo.townships import ( 21 clean_code, 22 clean_town, 23 strip_suffix, 24 strip_region, 25 toggle_suffix, 26 extract_alias 27) 28 29yaml = YAML() 30 31class MatchError(Exception): 32 def __init__(self, message): 33 super().__init__(message) 34 35@dataclass 36class TownReference: 37 name: str 38 geocode: str 39 gnis_id: int 40 town_type: TownType 41 county: County 42 cousub: Cousub 43 aliases: List[str] 44 processed: Optional[bool] = False 45 46 def __post_init__(self): 47 if not self.processed: 48 self._clean_aliases() 49 self._infer_aliases() 50 self.processed = True 51 52 def _clean_aliases(self): 53 aliases = sum(self.aliases, []) 54 aliases = map(str.upper, filter(None, aliases)) 55 self.aliases = list(set(aliases)) 56 57 def _infer_aliases(self): 58 aliases = self.aliases 59 60 aliases.extend(list(map(clean_code, aliases))) 61 aliases.extend(list(map(clean_town, aliases))) 62 aliases.extend(list(map(strip_suffix, aliases))) 63 aliases.extend(list(map(strip_region, aliases))) 64 aliases.extend(list(map(strip_region, aliases))) # 2x 65 66 township_aliases = filter(None, map(extract_alias, aliases)) 67 aliases.extend(list(township_aliases)) 68 69 if self.town_type in (TownType.UNORGANIZED, TownType.ISLAND): 70 aliases.extend(list(map(toggle_suffix, aliases))) 71 72 self.aliases = list(set(aliases)) 73 self.aliases.sort() 74 75 @staticmethod 76 def json_object_hook(json_record, processed = False): 77 return TownReference( 78 name = json_record['town'], 79 geocode = json_record['town_geocode'], 80 gnis_id = json_record['gnis_id'], 81 town_type = TownType(json_record['geotype']), 82 county = County( 83 fips = json_record['county_fips'], 84 name = json_record['county_name'], 85 code = json_record['sos_county'] 86 ), 87 cousub = Cousub( 88 geocode = json_record['cousub_geocode'], 89 name = json_record['cousub_name'], 90 basename = json_record['cousub_basename'], 91 geoclass = json_record['classfp'] 92 ), 93 aliases = [ 94 [ 95 json_record['maine_gis_name'], 96 json_record['town'], 97 json_record['cousub_basename'], 98 json_record['voting_name'], 99 json_record['tribal_name'] 100 ], 101 json_record['gnis_variants'], 102 json_record['historical_names'], 103 json_record['misspellings'], 104 json_record['islands'] 105 ], 106 processed = processed 107 ) 108 109@dataclass(frozen=True) 110class TownAlias: 111 """ A lightweight frozen container holding the minimum elements required for matching. 112 """ 113 name: str 114 county_fips: Optional[int] = None 115 116@dataclass 117class TownDatabase: 118 data: List[TownReference] = None 119 processed: bool = False 120 121 YAML_PATH: ClassVar[Path] = TOWNSHIPS_YAML 122 JSON_PATH: ClassVar[Path] = TOWNSHIPS_JSON 123 124 def __post_init__(self): 125 if self.processed is False: 126 self._process_data() 127 self._validate_data() 128 129 @classmethod 130 def build(cls): 131 file_path = cls.YAML_PATH 132 # first call 133 if Path(file_path).exists(): 134 return cls.load_from_yaml(file_path) 135 # subsequent calls 136 else: 137 towndb = cls.create_from_raw_data() 138 towndb.save_to_yaml(file_path) 139 return towndb 140 141 @classmethod 142 def create_from_raw_data(cls, json_path: Path = None): 143 import json 144 145 if json_path is None: 146 json_path = cls.JSON_PATH 147 148 with open(json_path) as file: 149 towns = json.load(file, object_hook = TownReference.json_object_hook) 150 return cls(towns, processed = False) 151 152 @classmethod 153 def load_from_yaml(cls, file_path = None): 154 file_path = file_path or cls.YAML_PATH 155 156 with open(file_path, 'r') as f: 157 data = yaml.load(f) 158 towns = [] 159 160 for yml in data['towns']: 161 town = TownReference( 162 name = yml['name'], 163 geocode = yml['geocode'], 164 gnis_id = yml['gnis_id'], 165 town_type = TownType(yml['town_type']), 166 county = County( 167 fips = yml['county']['fips'], 168 name = yml['county']['name'], 169 code = yml['county']['code'] 170 ), 171 cousub = Cousub( 172 geocode = yml['cousub']['geocode'], 173 name = yml['cousub']['name'], 174 basename = yml['cousub']['basename'], 175 geoclass = yml['cousub']['geoclass'] 176 ), 177 aliases = yml['aliases'], 178 processed = True 179 ) 180 towns.append(town) 181 182 return cls(towns, processed = True) 183 184 def save_to_yaml(self, file_path = None): 185 file_path = file_path or self.YAML_PATH 186 187 serializable_data = { 188 'towns': [ 189 { 190 'name': town.name, 191 'geocode': town.geocode, 192 'town_type': town.town_type.value, 193 'gnis_id': town.gnis_id, 194 'county': { 195 'fips': town.county.fips, 196 'name': town.county.name, 197 'code': town.county.code 198 }, 199 'cousub' : { 200 'geocode': town.cousub.geocode, 201 'name': town.cousub.name, 202 'basename': town.cousub.basename, 203 'geoclass': town.cousub.geoclass 204 }, 205 'aliases': town.aliases 206 } for town in self.data 207 ] 208 } 209 210 file_path.parent.mkdir(parents = True, exist_ok = True) 211 212 with open(file_path, 'w') as f: 213 yaml.dump(serializable_data, f) 214 215 def _process_data(self): 216 if self.data is not None: 217 self._remove_duplicate_aliases() 218 self.data.sort(key=lambda x: x.name) 219 self.processed = True 220 221 def _validate_data(self): 222 geocodes = [town.geocode for town in self.data] 223 if not all(geocodes): 224 raise ValueError("Missing geocodes in source data") 225 elif len(geocodes) != len(set(geocodes)): 226 raise ValueError("Non-unique geocodes in source data") 227 228 def _remove_duplicate_aliases(self): 229 for town in self.data: 230 town.aliases = [ 231 alias_name for alias_name in town.aliases 232 if TownAlias(alias_name, town.county.fips) in self.alias_lookup.keys() 233 ] 234 235 @cached_property 236 def _suggested_aliases(self) -> List[TownAlias]: 237 all = [] 238 for town in self.data: 239 for alias_name in town.aliases: 240 all.append(TownAlias(alias_name)) 241 all.append(TownAlias(alias_name, town.county.fips)) 242 return all 243 244 @cached_property 245 def _unique_aliases(self) -> List[TownAlias]: 246 counts = {} 247 for alias in self._suggested_aliases: 248 counts[alias] = counts.get(alias, 0) + 1 249 return [alias for alias, count in counts.items() if count == 1] 250 251 @cached_property 252 def alias_lookup(self) -> Dict[TownAlias, TownReference]: 253 records = {} 254 for town in self.data: 255 canonical = town.name.upper() 256 257 for alias_name in town.aliases: 258 state_alias = TownAlias(alias_name) 259 county_alias = TownAlias(alias_name, town.county.fips) 260 261 for alias in (state_alias, county_alias): 262 if alias in self._unique_aliases or alias_name == canonical: 263 records[alias] = town 264 return records 265 266 def search(self, **kwargs) -> List[TownReference]: 267 return [ 268 town for town in self.data 269 if all(getattr(town, k) == v for k, v in kwargs.items()) 270 ] 271 272 def match( 273 self, 274 town: str, 275 county_fips: int = None, 276 cleaned: bool = False, 277 strict: bool = False 278 ) -> TownReference: 279 """ 280 Clean and match a town name to the alias database and return the `TownReference` object. 281 282 For faster matches on already clean data, 283 pass `cleaned = True` to match without cleaning. 284 285 Args: 286 town: A single town or township name 287 county_fips: Integer code for county. If used, will improve match rate. 288 cleaned: True if the town name is already clean, False if it should be cleaned. 289 strict: True if exception should be raised when a match fails 290 291 Examples: 292 >>> towndb = TownDatabase.build() 293 >>> towndb.match('Cross Lake Twp (T17 R5)').name 294 'Cross Lake Twp' 295 >>> towndb.match('Prentiss Twp', cleaned = True) is None 296 True 297 >>> towndb.match('Prentiss Twp', county_fips = 19, cleaned = True).name 298 'Prentiss Twp T7 R3 NBPP' 299 """ 300 town_name = town.upper() if cleaned else clean_town(town.upper()) 301 302 def lazy_get_code(unmatched_town: str): 303 township_code = clean_code(unmatched_town) 304 if township_code != unmatched_town: 305 return township_code 306 307 def lazy_get_alias(unmatched_town: str): 308 township_alias = extract_alias(unmatched_town) 309 if township_alias != unmatched_town: 310 return township_alias 311 312 # lazy evaluation 313 names = [ 314 lambda: town_name, 315 lambda: lazy_get_code(town_name), 316 lambda: lazy_get_alias(town_name) 317 ] 318 319 for get_name in names: 320 name = get_name() 321 if name: 322 state_alias = TownAlias(name) 323 state_match = self.alias_lookup.get(state_alias) 324 325 if state_match: 326 return state_match 327 328 if county_fips is None: 329 continue 330 331 county_alias = TownAlias(name, county_fips) 332 county_match = self.alias_lookup.get(county_alias) 333 334 if county_match: 335 return county_match 336 337 if strict: 338 message = f'No match found for {town}.' 339 raise MatchError(message = message) 340 341 def canonical_name(self, town: str, county_fips: int = None, cleaned: bool = False) -> str: 342 """ 343 Match a town to the alias database and return the canonical name only. 344 345 Convenience method; equivalent to `TownDatabase.build().match(...).name`. 346 347 For faster matches on already clean data, 348 pass `cleaned = True` to match without cleaning. 349 350 Args: 351 town: A single town or township name 352 county_fips: Integer code for county. If used, will improve match rate. 353 cleaned: True if the town name is already clean, False if it should be cleaned. 354 355 Examples: 356 >>> towndb = TownDatabase.build() 357 >>> towndb.canonical_name('Cross Lake Twp (T17 R5)') 358 'Cross Lake Twp' 359 >>> towndb.canonical_name('Soldiertown Twp') is None 360 True 361 >>> towndb.canonical_name('Soldiertown Twp', county_fips = 25) 362 'Soldiertown Twp T2 R3 NBKP' 363 """ 364 match = self.match(town, county_fips, cleaned = cleaned) 365 if match: 366 return match.name 367 368@cache 369def get_town_database() -> TownDatabase: 370 return TownDatabase.build()
@dataclass
class
TownReference:
36@dataclass 37class TownReference: 38 name: str 39 geocode: str 40 gnis_id: int 41 town_type: TownType 42 county: County 43 cousub: Cousub 44 aliases: List[str] 45 processed: Optional[bool] = False 46 47 def __post_init__(self): 48 if not self.processed: 49 self._clean_aliases() 50 self._infer_aliases() 51 self.processed = True 52 53 def _clean_aliases(self): 54 aliases = sum(self.aliases, []) 55 aliases = map(str.upper, filter(None, aliases)) 56 self.aliases = list(set(aliases)) 57 58 def _infer_aliases(self): 59 aliases = self.aliases 60 61 aliases.extend(list(map(clean_code, aliases))) 62 aliases.extend(list(map(clean_town, aliases))) 63 aliases.extend(list(map(strip_suffix, aliases))) 64 aliases.extend(list(map(strip_region, aliases))) 65 aliases.extend(list(map(strip_region, aliases))) # 2x 66 67 township_aliases = filter(None, map(extract_alias, aliases)) 68 aliases.extend(list(township_aliases)) 69 70 if self.town_type in (TownType.UNORGANIZED, TownType.ISLAND): 71 aliases.extend(list(map(toggle_suffix, aliases))) 72 73 self.aliases = list(set(aliases)) 74 self.aliases.sort() 75 76 @staticmethod 77 def json_object_hook(json_record, processed = False): 78 return TownReference( 79 name = json_record['town'], 80 geocode = json_record['town_geocode'], 81 gnis_id = json_record['gnis_id'], 82 town_type = TownType(json_record['geotype']), 83 county = County( 84 fips = json_record['county_fips'], 85 name = json_record['county_name'], 86 code = json_record['sos_county'] 87 ), 88 cousub = Cousub( 89 geocode = json_record['cousub_geocode'], 90 name = json_record['cousub_name'], 91 basename = json_record['cousub_basename'], 92 geoclass = json_record['classfp'] 93 ), 94 aliases = [ 95 [ 96 json_record['maine_gis_name'], 97 json_record['town'], 98 json_record['cousub_basename'], 99 json_record['voting_name'], 100 json_record['tribal_name'] 101 ], 102 json_record['gnis_variants'], 103 json_record['historical_names'], 104 json_record['misspellings'], 105 json_record['islands'] 106 ], 107 processed = processed 108 )
TownReference( name: str, geocode: str, gnis_id: int, town_type: mainegeo.entities.TownType, county: mainegeo.entities.County, cousub: mainegeo.entities.Cousub, aliases: List[str], processed: Optional[bool] = False)
town_type: mainegeo.entities.TownType
county: mainegeo.entities.County
cousub: mainegeo.entities.Cousub
@staticmethod
def
json_object_hook(json_record, processed=False):
76 @staticmethod 77 def json_object_hook(json_record, processed = False): 78 return TownReference( 79 name = json_record['town'], 80 geocode = json_record['town_geocode'], 81 gnis_id = json_record['gnis_id'], 82 town_type = TownType(json_record['geotype']), 83 county = County( 84 fips = json_record['county_fips'], 85 name = json_record['county_name'], 86 code = json_record['sos_county'] 87 ), 88 cousub = Cousub( 89 geocode = json_record['cousub_geocode'], 90 name = json_record['cousub_name'], 91 basename = json_record['cousub_basename'], 92 geoclass = json_record['classfp'] 93 ), 94 aliases = [ 95 [ 96 json_record['maine_gis_name'], 97 json_record['town'], 98 json_record['cousub_basename'], 99 json_record['voting_name'], 100 json_record['tribal_name'] 101 ], 102 json_record['gnis_variants'], 103 json_record['historical_names'], 104 json_record['misspellings'], 105 json_record['islands'] 106 ], 107 processed = processed 108 )
@dataclass
class
TownDatabase:
117@dataclass 118class TownDatabase: 119 data: List[TownReference] = None 120 processed: bool = False 121 122 YAML_PATH: ClassVar[Path] = TOWNSHIPS_YAML 123 JSON_PATH: ClassVar[Path] = TOWNSHIPS_JSON 124 125 def __post_init__(self): 126 if self.processed is False: 127 self._process_data() 128 self._validate_data() 129 130 @classmethod 131 def build(cls): 132 file_path = cls.YAML_PATH 133 # first call 134 if Path(file_path).exists(): 135 return cls.load_from_yaml(file_path) 136 # subsequent calls 137 else: 138 towndb = cls.create_from_raw_data() 139 towndb.save_to_yaml(file_path) 140 return towndb 141 142 @classmethod 143 def create_from_raw_data(cls, json_path: Path = None): 144 import json 145 146 if json_path is None: 147 json_path = cls.JSON_PATH 148 149 with open(json_path) as file: 150 towns = json.load(file, object_hook = TownReference.json_object_hook) 151 return cls(towns, processed = False) 152 153 @classmethod 154 def load_from_yaml(cls, file_path = None): 155 file_path = file_path or cls.YAML_PATH 156 157 with open(file_path, 'r') as f: 158 data = yaml.load(f) 159 towns = [] 160 161 for yml in data['towns']: 162 town = TownReference( 163 name = yml['name'], 164 geocode = yml['geocode'], 165 gnis_id = yml['gnis_id'], 166 town_type = TownType(yml['town_type']), 167 county = County( 168 fips = yml['county']['fips'], 169 name = yml['county']['name'], 170 code = yml['county']['code'] 171 ), 172 cousub = Cousub( 173 geocode = yml['cousub']['geocode'], 174 name = yml['cousub']['name'], 175 basename = yml['cousub']['basename'], 176 geoclass = yml['cousub']['geoclass'] 177 ), 178 aliases = yml['aliases'], 179 processed = True 180 ) 181 towns.append(town) 182 183 return cls(towns, processed = True) 184 185 def save_to_yaml(self, file_path = None): 186 file_path = file_path or self.YAML_PATH 187 188 serializable_data = { 189 'towns': [ 190 { 191 'name': town.name, 192 'geocode': town.geocode, 193 'town_type': town.town_type.value, 194 'gnis_id': town.gnis_id, 195 'county': { 196 'fips': town.county.fips, 197 'name': town.county.name, 198 'code': town.county.code 199 }, 200 'cousub' : { 201 'geocode': town.cousub.geocode, 202 'name': town.cousub.name, 203 'basename': town.cousub.basename, 204 'geoclass': town.cousub.geoclass 205 }, 206 'aliases': town.aliases 207 } for town in self.data 208 ] 209 } 210 211 file_path.parent.mkdir(parents = True, exist_ok = True) 212 213 with open(file_path, 'w') as f: 214 yaml.dump(serializable_data, f) 215 216 def _process_data(self): 217 if self.data is not None: 218 self._remove_duplicate_aliases() 219 self.data.sort(key=lambda x: x.name) 220 self.processed = True 221 222 def _validate_data(self): 223 geocodes = [town.geocode for town in self.data] 224 if not all(geocodes): 225 raise ValueError("Missing geocodes in source data") 226 elif len(geocodes) != len(set(geocodes)): 227 raise ValueError("Non-unique geocodes in source data") 228 229 def _remove_duplicate_aliases(self): 230 for town in self.data: 231 town.aliases = [ 232 alias_name for alias_name in town.aliases 233 if TownAlias(alias_name, town.county.fips) in self.alias_lookup.keys() 234 ] 235 236 @cached_property 237 def _suggested_aliases(self) -> List[TownAlias]: 238 all = [] 239 for town in self.data: 240 for alias_name in town.aliases: 241 all.append(TownAlias(alias_name)) 242 all.append(TownAlias(alias_name, town.county.fips)) 243 return all 244 245 @cached_property 246 def _unique_aliases(self) -> List[TownAlias]: 247 counts = {} 248 for alias in self._suggested_aliases: 249 counts[alias] = counts.get(alias, 0) + 1 250 return [alias for alias, count in counts.items() if count == 1] 251 252 @cached_property 253 def alias_lookup(self) -> Dict[TownAlias, TownReference]: 254 records = {} 255 for town in self.data: 256 canonical = town.name.upper() 257 258 for alias_name in town.aliases: 259 state_alias = TownAlias(alias_name) 260 county_alias = TownAlias(alias_name, town.county.fips) 261 262 for alias in (state_alias, county_alias): 263 if alias in self._unique_aliases or alias_name == canonical: 264 records[alias] = town 265 return records 266 267 def search(self, **kwargs) -> List[TownReference]: 268 return [ 269 town for town in self.data 270 if all(getattr(town, k) == v for k, v in kwargs.items()) 271 ] 272 273 def match( 274 self, 275 town: str, 276 county_fips: int = None, 277 cleaned: bool = False, 278 strict: bool = False 279 ) -> TownReference: 280 """ 281 Clean and match a town name to the alias database and return the `TownReference` object. 282 283 For faster matches on already clean data, 284 pass `cleaned = True` to match without cleaning. 285 286 Args: 287 town: A single town or township name 288 county_fips: Integer code for county. If used, will improve match rate. 289 cleaned: True if the town name is already clean, False if it should be cleaned. 290 strict: True if exception should be raised when a match fails 291 292 Examples: 293 >>> towndb = TownDatabase.build() 294 >>> towndb.match('Cross Lake Twp (T17 R5)').name 295 'Cross Lake Twp' 296 >>> towndb.match('Prentiss Twp', cleaned = True) is None 297 True 298 >>> towndb.match('Prentiss Twp', county_fips = 19, cleaned = True).name 299 'Prentiss Twp T7 R3 NBPP' 300 """ 301 town_name = town.upper() if cleaned else clean_town(town.upper()) 302 303 def lazy_get_code(unmatched_town: str): 304 township_code = clean_code(unmatched_town) 305 if township_code != unmatched_town: 306 return township_code 307 308 def lazy_get_alias(unmatched_town: str): 309 township_alias = extract_alias(unmatched_town) 310 if township_alias != unmatched_town: 311 return township_alias 312 313 # lazy evaluation 314 names = [ 315 lambda: town_name, 316 lambda: lazy_get_code(town_name), 317 lambda: lazy_get_alias(town_name) 318 ] 319 320 for get_name in names: 321 name = get_name() 322 if name: 323 state_alias = TownAlias(name) 324 state_match = self.alias_lookup.get(state_alias) 325 326 if state_match: 327 return state_match 328 329 if county_fips is None: 330 continue 331 332 county_alias = TownAlias(name, county_fips) 333 county_match = self.alias_lookup.get(county_alias) 334 335 if county_match: 336 return county_match 337 338 if strict: 339 message = f'No match found for {town}.' 340 raise MatchError(message = message) 341 342 def canonical_name(self, town: str, county_fips: int = None, cleaned: bool = False) -> str: 343 """ 344 Match a town to the alias database and return the canonical name only. 345 346 Convenience method; equivalent to `TownDatabase.build().match(...).name`. 347 348 For faster matches on already clean data, 349 pass `cleaned = True` to match without cleaning. 350 351 Args: 352 town: A single town or township name 353 county_fips: Integer code for county. If used, will improve match rate. 354 cleaned: True if the town name is already clean, False if it should be cleaned. 355 356 Examples: 357 >>> towndb = TownDatabase.build() 358 >>> towndb.canonical_name('Cross Lake Twp (T17 R5)') 359 'Cross Lake Twp' 360 >>> towndb.canonical_name('Soldiertown Twp') is None 361 True 362 >>> towndb.canonical_name('Soldiertown Twp', county_fips = 25) 363 'Soldiertown Twp T2 R3 NBKP' 364 """ 365 match = self.match(town, county_fips, cleaned = cleaned) 366 if match: 367 return match.name
TownDatabase( data: List[TownReference] = None, processed: bool = False)
YAML_PATH: ClassVar[pathlib.Path] =
PosixPath('/Users/lydia-rosekesich/repositories/maine-geography/src/mainegeo/data/townships.yaml')
JSON_PATH: ClassVar[pathlib.Path] =
PosixPath('/Users/lydia-rosekesich/repositories/maine-geography/src/mainegeo/data/townships.json')
@classmethod
def
create_from_raw_data(cls, json_path: pathlib.Path = None):
142 @classmethod 143 def create_from_raw_data(cls, json_path: Path = None): 144 import json 145 146 if json_path is None: 147 json_path = cls.JSON_PATH 148 149 with open(json_path) as file: 150 towns = json.load(file, object_hook = TownReference.json_object_hook) 151 return cls(towns, processed = False)
@classmethod
def
load_from_yaml(cls, file_path=None):
153 @classmethod 154 def load_from_yaml(cls, file_path = None): 155 file_path = file_path or cls.YAML_PATH 156 157 with open(file_path, 'r') as f: 158 data = yaml.load(f) 159 towns = [] 160 161 for yml in data['towns']: 162 town = TownReference( 163 name = yml['name'], 164 geocode = yml['geocode'], 165 gnis_id = yml['gnis_id'], 166 town_type = TownType(yml['town_type']), 167 county = County( 168 fips = yml['county']['fips'], 169 name = yml['county']['name'], 170 code = yml['county']['code'] 171 ), 172 cousub = Cousub( 173 geocode = yml['cousub']['geocode'], 174 name = yml['cousub']['name'], 175 basename = yml['cousub']['basename'], 176 geoclass = yml['cousub']['geoclass'] 177 ), 178 aliases = yml['aliases'], 179 processed = True 180 ) 181 towns.append(town) 182 183 return cls(towns, processed = True)
def
save_to_yaml(self, file_path=None):
185 def save_to_yaml(self, file_path = None): 186 file_path = file_path or self.YAML_PATH 187 188 serializable_data = { 189 'towns': [ 190 { 191 'name': town.name, 192 'geocode': town.geocode, 193 'town_type': town.town_type.value, 194 'gnis_id': town.gnis_id, 195 'county': { 196 'fips': town.county.fips, 197 'name': town.county.name, 198 'code': town.county.code 199 }, 200 'cousub' : { 201 'geocode': town.cousub.geocode, 202 'name': town.cousub.name, 203 'basename': town.cousub.basename, 204 'geoclass': town.cousub.geoclass 205 }, 206 'aliases': town.aliases 207 } for town in self.data 208 ] 209 } 210 211 file_path.parent.mkdir(parents = True, exist_ok = True) 212 213 with open(file_path, 'w') as f: 214 yaml.dump(serializable_data, f)
alias_lookup: Dict[mainegeo.matching.TownAlias, TownReference]
252 @cached_property 253 def alias_lookup(self) -> Dict[TownAlias, TownReference]: 254 records = {} 255 for town in self.data: 256 canonical = town.name.upper() 257 258 for alias_name in town.aliases: 259 state_alias = TownAlias(alias_name) 260 county_alias = TownAlias(alias_name, town.county.fips) 261 262 for alias in (state_alias, county_alias): 263 if alias in self._unique_aliases or alias_name == canonical: 264 records[alias] = town 265 return records
def
match( self, town: str, county_fips: int = None, cleaned: bool = False, strict: bool = False) -> TownReference:
273 def match( 274 self, 275 town: str, 276 county_fips: int = None, 277 cleaned: bool = False, 278 strict: bool = False 279 ) -> TownReference: 280 """ 281 Clean and match a town name to the alias database and return the `TownReference` object. 282 283 For faster matches on already clean data, 284 pass `cleaned = True` to match without cleaning. 285 286 Args: 287 town: A single town or township name 288 county_fips: Integer code for county. If used, will improve match rate. 289 cleaned: True if the town name is already clean, False if it should be cleaned. 290 strict: True if exception should be raised when a match fails 291 292 Examples: 293 >>> towndb = TownDatabase.build() 294 >>> towndb.match('Cross Lake Twp (T17 R5)').name 295 'Cross Lake Twp' 296 >>> towndb.match('Prentiss Twp', cleaned = True) is None 297 True 298 >>> towndb.match('Prentiss Twp', county_fips = 19, cleaned = True).name 299 'Prentiss Twp T7 R3 NBPP' 300 """ 301 town_name = town.upper() if cleaned else clean_town(town.upper()) 302 303 def lazy_get_code(unmatched_town: str): 304 township_code = clean_code(unmatched_town) 305 if township_code != unmatched_town: 306 return township_code 307 308 def lazy_get_alias(unmatched_town: str): 309 township_alias = extract_alias(unmatched_town) 310 if township_alias != unmatched_town: 311 return township_alias 312 313 # lazy evaluation 314 names = [ 315 lambda: town_name, 316 lambda: lazy_get_code(town_name), 317 lambda: lazy_get_alias(town_name) 318 ] 319 320 for get_name in names: 321 name = get_name() 322 if name: 323 state_alias = TownAlias(name) 324 state_match = self.alias_lookup.get(state_alias) 325 326 if state_match: 327 return state_match 328 329 if county_fips is None: 330 continue 331 332 county_alias = TownAlias(name, county_fips) 333 county_match = self.alias_lookup.get(county_alias) 334 335 if county_match: 336 return county_match 337 338 if strict: 339 message = f'No match found for {town}.' 340 raise MatchError(message = message)
Clean and match a town name to the alias database and return the TownReference object.
For faster matches on already clean data,
pass cleaned = True to match without cleaning.
Arguments:
- town: A single town or township name
- county_fips: Integer code for county. If used, will improve match rate.
- cleaned: True if the town name is already clean, False if it should be cleaned.
- strict: True if exception should be raised when a match fails
Examples:
>>> towndb = TownDatabase.build() >>> towndb.match('Cross Lake Twp (T17 R5)').name 'Cross Lake Twp' >>> towndb.match('Prentiss Twp', cleaned = True) is None True >>> towndb.match('Prentiss Twp', county_fips = 19, cleaned = True).name 'Prentiss Twp T7 R3 NBPP'
def
canonical_name(self, town: str, county_fips: int = None, cleaned: bool = False) -> str:
342 def canonical_name(self, town: str, county_fips: int = None, cleaned: bool = False) -> str: 343 """ 344 Match a town to the alias database and return the canonical name only. 345 346 Convenience method; equivalent to `TownDatabase.build().match(...).name`. 347 348 For faster matches on already clean data, 349 pass `cleaned = True` to match without cleaning. 350 351 Args: 352 town: A single town or township name 353 county_fips: Integer code for county. If used, will improve match rate. 354 cleaned: True if the town name is already clean, False if it should be cleaned. 355 356 Examples: 357 >>> towndb = TownDatabase.build() 358 >>> towndb.canonical_name('Cross Lake Twp (T17 R5)') 359 'Cross Lake Twp' 360 >>> towndb.canonical_name('Soldiertown Twp') is None 361 True 362 >>> towndb.canonical_name('Soldiertown Twp', county_fips = 25) 363 'Soldiertown Twp T2 R3 NBKP' 364 """ 365 match = self.match(town, county_fips, cleaned = cleaned) 366 if match: 367 return match.name
Match a town to the alias database and return the canonical name only.
Convenience method; equivalent to TownDatabase.build().match(...).name.
For faster matches on already clean data,
pass cleaned = True to match without cleaning.
Arguments:
- town: A single town or township name
- county_fips: Integer code for county. If used, will improve match rate.
- cleaned: True if the town name is already clean, False if it should be cleaned.
Examples:
>>> towndb = TownDatabase.build() >>> towndb.canonical_name('Cross Lake Twp (T17 R5)') 'Cross Lake Twp' >>> towndb.canonical_name('Soldiertown Twp') is None True >>> towndb.canonical_name('Soldiertown Twp', county_fips = 25) 'Soldiertown Twp T2 R3 NBKP'