This commit is contained in:
nq
2026-10-19 08:52:24 +00:00
parent eb3268cf78
commit 3433135790
13 changed files with 2480 additions and 2321 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Audit Garmin -> OSM POI mappings against an exact OsmAnd poi_types.xml.
No OBF creation. Exits 0 for ordinary data regressions unless --strict is requested.
The existence of a poi_type does NOT guarantee map icon visibility at any zoom.
"""
import argparse,csv,json,collections,sys
from pathlib import Path
from xml.etree import ElementTree as ET
def registry(path):
root=ET.parse(path).getroot()
main=collections.defaultdict(list);extras=collections.defaultdict(list)
for node in root.iter():
if node.tag not in ('poi_type','poi_additional'):continue
t=node.attrib.get('tag');v=node.attrib.get('value')
if not t or v is None:continue
d=main if node.tag=='poi_type' else extras
d[f'{t}={v}'].append(node.attrib)
return main,extras
def interpret(row,main,extra):
tags=[v.strip() for v in row['osm_tags'].split(';') if '=' in v]
supported=[];noindex=[];additional=[];missing=[]
for tv in tags:
candidates=main.get(tv,[])
if any(x.get('no_indx')!='true' for x in candidates):supported.append(tv)
elif candidates:noindex.append(tv)
elif tv in extra:additional.append(tv)
else:missing.append(tv)
if supported:kind='native_indexable_type'
elif noindex:kind='only_nonindexed_type'
elif additional:kind='additional_without_primary_type'
else:kind='no_matching_native_type'
is_terrain=(row.get('priority')=='terrain' or row.get('code')=='0x6600')
priority=row.get('priority','')
cnt=int(row.get('source_count') or 0)
if is_terrain:severity='terrain_exemption'
elif kind=='native_indexable_type':severity='ok'
elif priority=='critical':severity='critical'
elif cnt>=100:severity='high'
else:severity='low'
return {'code':row.get('code',''),'count':cnt,'priority':priority,'legend_bg':row.get('legend_bg',''),
'osm_tags':row.get('osm_tags',''), 'status':kind, 'severity':severity,
'indexable_poi_tags':';'.join(supported),'nonindexed_poi_tags':';'.join(noindex),
'additional_only_tags':';'.join(additional),'unrecognized_tags':';'.join(missing),
'sample_lat':row.get('example_lat',''),'sample_lon':row.get('example_lon',''),
'explanation':'Registry compatibility only; does not guarantee rendering zoom or OBF preservation'}
def main(argv=None):
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--mapping',required=True,help='Single canonical POI mapping CSV')
ap.add_argument('--poi-types',required=True,help='OsmAnd official poi_types.xml, preferably pinned version')
ap.add_argument('--report',default='poi-native-index-audit.csv')
ap.add_argument('--json',default='poi-native-index-audit.json')
ap.add_argument('--strict',action='store_true',help='Exit nonzero for unindexable critical/high types')
args=ap.parse_args(argv)
native,extra=registry(args.poi_types)
with open(args.mapping,encoding='utf8-sig',newline='') as f:
allrows=list(csv.DictReader(f))
# Name-conditional rows are not additional Garmin types; counting them as
# independent types would multiply source_count and inflate POI statistics.
rows=[r for r in allrows if not r.get('name_contains','')]
out=[interpret(r,native,extra) for r in rows]
with open(args.report,'w',newline='',encoding='utf8') as f:
w=csv.DictWriter(f,fieldnames=out[0].keys());w.writeheader();w.writerows(out)
stats=collections.Counter(r['status'] for r in out)
weighted=collections.Counter()
for r in out:weighted[r['status']]+=r['count']
risk=[r for r in out if r['severity'] in ('critical','high')]
payload={'type_counts':dict(stats),'weighted_source_counts':dict(weighted),'critical_or_high':len(risk),
'top_regressions':sorted(risk,key=lambda z:-z['count'])[:30],
'warning':'POI type registry check ONLY. Does not guarantee OBF map index presence, symbol visibility, zoom or Nearby settings.'}
Path(args.json).write_text(json.dumps(payload,ensure_ascii=False,indent=2),encoding='utf8')
print(f"Inspected {len(out)} Garmin types: {dict(stats)}, high/critical issues: {len(risk)}")
print(f"Wrote {args.report} and {args.json}")
return 2 if args.strict and risk else 0
if __name__=='__main__':sys.exit(main())