Package: uk-postcodes-parsing
uk-postcodes-parsingA Python package to parse UK postcodes from text. Useful in applications such as OCR and IDP.Installpipinstalluk-postcodes-parsingCapabilitiesSearch and parse UK postcode from text/OCR resultsExtract parts of the postcode: incode, outcode etc.Fix common mistakes in UK postcode OCRPostcode.outcode.incode.area.district.subDistrict.sector.unitAA9A 9AAAA9A9AAAAAA9AA9AAA9A 9AAA9A 9AAA9A9AAAA9A9AA9A 9AAA9 9AAA99AAAA9NoneA9 9AAA99 9AAA999AAAA99NoneA99 9AAAA9 9AAAA99AAAAAA9NoneAA9 9AAAA99 9AAAA999AAAAAA99NoneAA99 9AAUtilities to validate postcodeUpdated to May 2023: Validate postcode against ~1.8M UK postcodes from the ONS Postcode DirectoryUsageParsing text to get a list of postcodes.>>>fromuk_postcodes_parsingimportukpostcode>>>corpus="this is a check to see if we can get post codes liek thia ec1r 1ub , and that e3 4ss. But also eh16 50y and ei412">>>postcodes=ukpostcode.parse_from_corpus(corpus)INFO:uk-postcodes-parsing:Found2postcodesincorpus>>>postcodes[Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='ec1r 1ub',postcode='EC1R 1UB',incode='1UB',outcode='EC1R',area='EC',district='EC1',sub_district='EC1R',sector='EC1R 1',unit='UB'),Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='e3 4ss',postcode='E3 4SS',incode='4SS',outcode='E3',area='E',district='E3',sub_district=None,sector='E3 4',unit='SS')]Optional auto-correct: Attempt correcting common mistakes in postcodes such as reading "O" and "0" and vice-versa.>>>fromuk_postcodes_parsingimportukpostcode>>>corpus="this is a check to see if we can get post codes liek thia ec1r 1ub , and that e3 4ss. But also eh16 50y and ei412">>>postcodes=ukpostcode.parse_from_corpus(corpus,attempt_fix=True)INFO:uk-postcodes-parsing:Found3postcodesincorpusINFO:uk-postcodes-parsing:PostcodeFixed:'eh16 50y'=>'EH16 5OY'You can also do an undertermisitic postcode auto-correct where if there is more than one possible answer, all answers are returned.>>>postcodes=ukpostcode.parse_from_corpus("OOO 4SS",attempt_fix=True,try_all_fix_options=True)>>postcodes# "O00 4SS", "OO0 4SS", and "O0O 4SS"[Postcode(is_in_ons_postcode_directory=False,fix_distance=-2,original='OOO 4SS',postcode='O00 4SS',incode='4SS',outcode='O00',area='O',district='O00',sub_district=None,sector='O00 4',unit='SS'),Postcode(is_in_ons_postcode_directory=False,fix_distance=-1,original='OOO 4SS',postcode='OO0 4SS',incode='4SS',outcode='OO0',area='OO',district='OO0',sub_district=None,sector='OO0 4',unit='SS'),Postcode(is_in_ons_postcode_directory=False,fix_distance=-1,original='OOO 4SS',postcode='O0O 4SS',incode='4SS',outcode='O0O',area='O',district='O0',sub_district='O0O',sector='O0O 4',unit='SS')]Parsing>>>fromuk_postcodes_parsingimportukpostcode>>>ukpostcode.parse("EC1r 1ub")Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='EC1r 1ub',postcode='EC1R 1UB',incode='1UB',outcode='EC1R',area='EC',district='EC1',sub_district='EC1R',sector='EC1R 1',unit='UB')>>>ukpostcode.parse("EH16 50Y")INFO:uk-postcodes-parsing:PostcodeFixed:'EH16 50Y'=>'EH16 5OY'Postcode(is_in_ons_postcode_directory=False,fix_distance=-1,original='EH16 50Y',postcode='EH16 5OY',incode='5OY',outcode='EH16',area='EH',district='EH16',sub_district=None,sector='EH16 5',unit='OY')>>>ukpostcode.parse("EH16 50Y",attempt_fix=False)# Don't attempt fixes during parsingERROR:uk-postcodes-parsing:Failedtoparsepostcode>>>ukpostcode.parse("0W1")ERROR:uk-postcodes-parsing:UnabletofixpostcodeERROR:uk-postcodes-parsing:FailedtoparsepostcodeValidity check>>>fromuk_postcodes_parsingimportpostcode_utils>>>postcode_utils.is_valid("0W1 0AA")False>>>postcode_utils.is_valid("OW1 0AA")TrueFixing>>>fromuk_postcodes_parsing.fiximportfix>>>fix("0W1 OAA")'OW1 0AA'Validate against ONS Postcode directory (1.7M+ UK postcode upto Nov 2022)>>>ukpostcode.is_in_ons_postcode_directory("EC1R 1UB")True>>>ukpostcode.is_in_ons_postcode_directory("ec1r 1ub")# Expects normalised format (caps + space)FalsePostcode class definition@dataclass(order=True)classPostcode:# Calculate post initializationis_in_ons_postcode_directory:bool=field(init=False)fix_distance:int=field(init=False)# raw textoriginal:str# The rest of the fields are parsed from the postcode using regexpostcode:strincode:stroutcode:strarea:strdistrict:strsub_district:Union[str,None]sector:strunit:str2 fileds calculated after init of classis_in_ons_postcode_directory: Checked against theONS Postcode Directoryfix_distance: A measure of number of characters changed from raw text. Each character fix adds a -1 (negative one) to this field.E.g.SW1A OAA=>SW1A 0AAhas fix_distance=-1. Where as,SWIA OAA=>SW1A 0AAhas fix_distance=-2.These fields are particularly helpful when usingparse_from_corpuswithattempt_fix=Truewhich might return false positives. They can be used as proxy for confidence on which parsed postcodes are correct.E.g. If you parse"send the parcel back to one of the following postcodes: EC1R 1UB or EH16 5AY.withattempt_fix:>>>corpus="send the parcel back to one of the following postcodes: ECIR 1UB or EH16 5AY">>>postcodes=ukpostcode.parse_from_corpus(corpus,attempt_fix=True)INFO:uk-postcodes-parsing:Found4postcodesincorpusINFO:uk-postcodes-parsing:PostcodeFixed:'to one'=>'T0 0NE'INFO:uk-postcodes-parsing:PostcodeFixed:'llowing'=>'LL0W 1NG'INFO:uk-postcodes-parsing:PostcodeFixed:'ecir 1ub'=>'EC1R 1UB'>>>postcodes# you get false positives[Postcode(is_in_ons_postcode_directory=False,fix_distance=-2,original='to one',postcode='T0 0NE',incode='0NE',outcode='T0',area='T',district='T0',sub_district=None,sector='T0 0',unit='NE'),Postcode(is_in_ons_postcode_directory=False,fix_distance=-2,original='llowing',postcode='LL0W 1NG',incode='1NG',outcode='LL0W',area='LL',district='LL0',sub_district='LL0W',sector='LL0W 1',unit='NG'),Postcode(is_in_ons_postcode_directory=True,fix_distance=-1,original='ecir 1ub',postcode='EC1R 1UB',incode='1UB',outcode='EC1R',area='EC',district='EC1',sub_district='EC1R',sector='EC1R 1',unit='UB'),Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='eh16 5ay',postcode='EH16 5AY',incode='5AY',outcode='EH16',area='EH',district='EH16',sub_district=None,sector='EH16 5',unit='AY')]You can sort a list of postcodes and chose the first n as needed:>>>sorted(postcodes,reverse=True)[Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='eh16 5ay',postcode='EH16 5AY',incode='5AY',outcode='EH16',area='EH',district='EH16',sub_district=None,sector='EH16 5',unit='AY'),Postcode(is_in_ons_postcode_directory=True,fix_distance=-1,original='ecir 1ub',postcode='EC1R 1UB',incode='1UB',outcode='EC1R',area='EC',district='EC1',sub_district='EC1R',sector='EC1R 1',unit='UB'),Postcode(is_in_ons_postcode_directory=False,fix_distance=-2,original='to one',postcode='T0 0NE',incode='0NE',outcode='T0',area='T',district='T0',sub_district=None,sector='T0 0',unit='NE'),Postcode(is_in_ons_postcode_directory=False,fix_distance=-2,original='llowing',postcode='LL0W 1NG',incode='1NG',outcode='LL0W',area='LL',district='LL0',sub_district='LL0W',sector='LL0W 1',unit='NG')]Or:>>>list(filter(lambdapostcode:postcode.is_in_ons_postcode_directory,postcodes))[Postcode(is_in_ons_postcode_directory=True,fix_distance=-1,original='ecir 1ub',postcode='EC1R 1UB',incode='1UB',outcode='EC1R',area='EC',district='EC1',sub_district='EC1R',sector='EC1R 1',unit='UB'),Postcode(is_in_ons_postcode_directory=True,fix_distance=0,original='eh16 5ay',postcode='EH16 5AY',incode='5AY',outcode='EH16',area='EH',district='EH16',sub_district=None,sector='EH16 5',unit='AY')]raw_text: To keep track of the original string without formatting changes and auto-fixes.8 fileds are parsed using regexTestingpytesttests/Updating this library with newer version of ONS postcode directoryThis library has been updated with May 2023 ONS postcode directory. To update this to a newer, version, see:process_onspd.ipynb.Similar workThis package started as a Python replica of the postcode.io JavaScript library:https://github.com/ideal-postcodes/postcode