"""Extract explicit OSM addresses. Never infer a postcode or expand number ranges."""
import collections,gzip,hashlib,json,re,sys
from pathlib import Path
import osmium

def normalize(tags):
 def value(k,n=255):
  v=' '.join(tags.get('addr:'+k,'').split())
  if len(v)>n or any(ord(c)<32 for c in v):raise ValueError('invalid field')
  return v
 pc=value('postcode',32).replace(' ','').upper()
 if not re.fullmatch(r'(?:GIR0AA|[A-Z]{1,2}[0-9][A-Z0-9]?[0-9][A-Z]{2})',pc):return None
 # UK only; extract includes NI, no dependency on the GB-only ONS postcode table.
 if pc.startswith(('GY','JE','IM')):return None
 street=value('street');place=value('place')
 if not street and not place:return None
 return dict(house_number=value('housenumber',100),house_name=value('housename'),unit=value('unit',100),street=street,place=place,city=value('city') or value('town') or value('village'),postcode=pc[:-3]+' '+pc[-3:])

class Addresses(osmium.SimpleHandler):
 def __init__(self,out):super().__init__();self.out=out;self.stats=collections.Counter()
 def emit(self,obj,kind):
  if 'addr:postcode' not in obj.tags:return
  self.stats['postcode_tagged_objects']+=1
  if 'addr:interpolation' in obj.tags:self.stats['interpolation_excluded']+=1;return
  try:r=normalize(obj.tags)
  except ValueError:self.stats['invalid_fields']+=1;return
  if r is None:self.stats['incomplete_or_invalid']+=1;return
  lat=lon=None;method='unavailable'
  if kind=='node' and obj.location.valid():lat=obj.location.lat;lon=obj.location.lon;method='mapped_point'
  elif kind=='way':
   coords=[(n.lat,n.lon) for n in obj.nodes if n.location.valid()]
   if coords and len(coords)==len(obj.nodes):
    lat=(min(p[0] for p in coords)+max(p[0] for p in coords))/2
    lon=(min(p[1] for p in coords)+max(p[1] for p in coords))/2
    method='geometry_bbox_centre'
  if lat is not None and not(49<=lat<=62 and -9<=lon<=3):self.stats['outside_uk_bounds']+=1;return
  r.update(osm_id=f'{kind}/{obj.id}',latitude=lat,longitude=lon,position_method=method)
  self.out.write(json.dumps(r,ensure_ascii=False)+'\n');self.stats['rows']+=1
  if r['house_number']:self.stats['with_house_number']+=1
  if r['street'].lower()=='pym road':self.stats['pym_road']+=1
 def node(self,n):self.emit(n,'node')
 def way(self,w):self.emit(w,'way')
 def relation(self,r):self.emit(r,'relation')

def main(root):
 root=Path(root);source=root/'united-kingdom-261013.osm.pbf';filtered=root/'address-objects.osm.pbf'
 with gzip.open(root/'addresses.jsonl.gz.part','wt',encoding='utf8') as out:
  h=Addresses(out);h.apply_file(str(filtered),locations=True,idx='sparse_mem_array')
 if not h.stats['rows']:raise ValueError('Empty address extract')
 (root/'addresses.jsonl.gz.part').rename(root/'addresses.jsonl.gz')
 path=root/'addresses.jsonl.gz'
 def sha(p):
  with p.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
 m=dict(source='osm_explicit_addresses',release='Geofabrik UK 2026-10-13',file=path.name,sha256=sha(path),rows=h.stats['rows'],stats=dict(h.stats),licence='ODbL-1.0',attribution='© OpenStreetMap contributors; ODbL 1.0; extract by Geofabrik',raw={'url':'https://download.geofabrik.de/europe/united-kingdom-261013.osm.pbf','sha256':sha(source)},notes='Explicit co-occurring address tags only. Incomplete community coverage; not postal verification. No inferred postcode or interpolated house numbers. Way positions are bounding-box centres; relations may have no position.')
 (root/'addresses.manifest.json').write_text(json.dumps(m,indent=2));print(json.dumps(m),flush=True)
if __name__=='__main__':main(sys.argv[1])
