From 8dd6a49bc65e3e84bd84fb284df6c2cdc82b3ec7 Mon Sep 17 00:00:00 2001 From: Ian Dees Date: Sat, 22 Aug 2026 07:39:47 -0500 Subject: [PATCH] add KML support to conform pipeline --- openaddr/conform.py | 26 ++++++++++- openaddr/tests/conform.py | 30 +++++++++++++ openaddr/tests/conforms/lake-man-kml.json | 18 ++++++++ openaddr/tests/conforms/lake-man.kml | 54 +++++++++++++++++++++++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 openaddr/tests/conforms/lake-man-kml.json create mode 100644 openaddr/tests/conforms/lake-man.kml diff --git a/openaddr/conform.py b/openaddr/conform.py index 80f73e7a..d9cf83c3 100644 --- a/openaddr/conform.py +++ b/openaddr/conform.py @@ -427,6 +427,28 @@ def find_source_path(data_source, source_paths): return c _L.warning("Source names file %s but could not find it", source_file_name) return None + elif format_string == "kml": + candidates = [] + for fn in source_paths: + basename, ext = os.path.splitext(fn) + if ext.lower() == ".kml": + candidates.append(fn) + if len(candidates) == 0: + _L.warning("No KML found in %s", source_paths) + return None + elif len(candidates) == 1: + _L.debug("Selected %s for source", candidates[0]) + return candidates[0] + else: + if "file" not in conform: + _L.warning("Multiple KML files found, but source has no file attribute.") + return None + source_file_name = conform["file"] + for c in candidates: + if source_file_name == os.path.basename(c): + return c + _L.warning("Source names file %s but could not find it", source_file_name) + return None elif format_string == "xml": # Return file if it's specified, else return the first .gml file we find if "file" in conform: @@ -1197,7 +1219,7 @@ def extract_to_source_csv(source_config, source_path, extract_path): format_string = source_config.data_source["conform"]['format'] protocol_string = source_config.data_source['protocol'] - if format_string in ("shapefile", "xml", "gdb", "gpkg"): + if format_string in ("shapefile", "xml", "gdb", "gpkg", "kml"): ogr_source_path = normalize_ogr_filename_case(source_path) ogr_source_to_csv(source_config, ogr_source_path, extract_path) elif format_string == "csv": @@ -1240,7 +1262,7 @@ def conform_cli(source_config, source_path, dest_path): format_string = source_config.data_source["conform"].get('format') - if not format_string in ["shapefile", "geojson", "csv", "xml", "gdb", "gpkg"]: + if not format_string in ["shapefile", "geojson", "csv", "xml", "gdb", "gpkg", "kml"]: _L.warning("Skipping file with unknown conform: %s", source_path) return 1 diff --git a/openaddr/tests/conform.py b/openaddr/tests/conform.py index bec50520..16f497be 100644 --- a/openaddr/tests/conform.py +++ b/openaddr/tests/conform.py @@ -1806,6 +1806,36 @@ def test_lake_man_gpkg(self): self.assertEqual(rows[5]['properties']['number'], '5115') self.assertEqual(rows[5]['properties']['street'], 'OLD MILL RD') + def test_lake_man_kml(self): + with open(os.path.join(self.conforms_dir, "lake-man-kml.json")) as file: + source_config = SourceConfig(json.load(file), "addresses", "default") + source_path = os.path.join(self.conforms_dir, "lake-man.kml") + dest_path = os.path.join(self.testdir, 'lake-man-kml-conformed.csv') + + rc = conform_cli(source_config, source_path, dest_path) + self.assertEqual(0, rc) + + with open(dest_path) as fp: + rows = list(map(json.loads, list(fp))) + + self.assertEqual('Point', rows[0]['geometry']['type']) + self.assertAlmostEqual(-122.2592497, rows[0]['geometry']['coordinates'][0], places=4) + self.assertAlmostEqual(37.8026126, rows[0]['geometry']['coordinates'][1], places=4) + + self.assertEqual(6, len(rows)) + self.assertEqual(rows[0]['properties']['number'], '5115') + self.assertEqual(rows[0]['properties']['street'], 'FRUITED PLAINS LN') + self.assertEqual(rows[1]['properties']['number'], '5121') + self.assertEqual(rows[1]['properties']['street'], 'FRUITED PLAINS LN') + self.assertEqual(rows[2]['properties']['number'], '5133') + self.assertEqual(rows[2]['properties']['street'], 'FRUITED PLAINS LN') + self.assertEqual(rows[3]['properties']['number'], '5126') + self.assertEqual(rows[3]['properties']['street'], 'FRUITED PLAINS LN') + self.assertEqual(rows[4]['properties']['number'], '5120') + self.assertEqual(rows[4]['properties']['street'], 'FRUITED PLAINS LN') + self.assertEqual(rows[5]['properties']['number'], '5115') + self.assertEqual(rows[5]['properties']['street'], 'OLD MILL RD') + def test_lake_man_split(self): rc, dest_path = self._run_conform_on_source('lake-man-split', 'shp') self.assertEqual(0, rc) diff --git a/openaddr/tests/conforms/lake-man-kml.json b/openaddr/tests/conforms/lake-man-kml.json new file mode 100644 index 00000000..bca93cde --- /dev/null +++ b/openaddr/tests/conforms/lake-man-kml.json @@ -0,0 +1,18 @@ +{ + "schema": 2, + "layers": { + "addresses": [{ + "name": "default", + "data": "http://fake-web/lake-man.kml", + "cache": "http://fake-cache/lake-man.kml", + "protocol": "http", + "conform": { + "lon": "X", + "lat": "Y", + "number": "NUMBER", + "street": "STRNAME", + "format": "kml" + } + }] + } +} diff --git a/openaddr/tests/conforms/lake-man.kml b/openaddr/tests/conforms/lake-man.kml new file mode 100644 index 00000000..a841d361 --- /dev/null +++ b/openaddr/tests/conforms/lake-man.kml @@ -0,0 +1,54 @@ + + + + lake-man + + 5115 FRUITED PLAINS LN + + 5115 + FRUITED PLAINS LN + + -122.259249687195,37.8026126376074 + + + 5121 FRUITED PLAINS LN + + 5121 + FRUITED PLAINS LN + + -122.256717681885,37.8025278661215 + + + 5133 FRUITED PLAINS LN + + 5133 + FRUITED PLAINS LN + + -122.257940769196,37.802968676786 + + + 5126 FRUITED PLAINS LN + + 5126 + FRUITED PLAINS LN + + -122.258970737457,37.8007476424409 + + + 5120 FRUITED PLAINS LN + + 5120 + FRUITED PLAINS LN + + -122.256953716278,37.800713733002 + + + 5115 OLD MILL RD + + 5115 + OLD MILL RD + + -122.257640361786,37.8043589085714 + + +