From e2b3e47aa9e670b058410510638ee07ee413449e Mon Sep 17 00:00:00 2001 From: Suresh Devalapalli Date: Tue, 24 Feb 2026 14:25:14 -0800 Subject: [PATCH 1/3] potential fix for 2942 --- .gitignore | 2 + src/example.py | 23 ++++- .../serializer/osm/osm_graph.py | 40 ++++++-- .../serializer/osm/osm_normalizer.py | 91 +++++++++++++++---- tests/unit_tests/test_files/bug_2942.xml | 37 ++++++++ .../test_serializer/test_osm_graph.py | 59 +++++++++++- .../test_osm_osm_normalizer.py | 43 ++++++++- 7 files changed, 264 insertions(+), 31 deletions(-) create mode 100644 tests/unit_tests/test_files/bug_2942.xml diff --git a/.gitignore b/.gitignore index 0170abc..dfc33c2 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ cython_debug/ reports/ output test_results + +.DS_Store \ No newline at end of file diff --git a/src/example.py b/src/example.py index ccf7f7a..8f5e098 100644 --- a/src/example.py +++ b/src/example.py @@ -1,6 +1,7 @@ import os import asyncio from osm_osw_reformatter import Formatter +import argparse ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -27,6 +28,24 @@ def osw_convert(): # f.cleanup() +# if __name__ == '__main__': +# asyncio.run(osm_convert()) +# osw_convert() + +def main(): + parser = argparse.ArgumentParser(description='Convert between OSM and OSW') + parser.add_argument('-i', '--input', required=True, help='input file path') + parser.add_argument('-o', '--output', required=True, help='output directory') + parser.add_argument('-s', '--mode', required=True, choices=['OSW2OSM', 'OSM2OSW'], help='conversion mode') + args = parser.parse_args() + + os.makedirs(args.output, exist_ok=True) + f = Formatter(workdir=args.output, file_path=args.input) + + if args.mode == 'OSM2OSW': + asyncio.run(f.osm2osw()) + else: + f.osw2osm() + if __name__ == '__main__': - asyncio.run(osm_convert()) - osw_convert() + main() \ No newline at end of file diff --git a/src/osm_osw_reformatter/serializer/osm/osm_graph.py b/src/osm_osw_reformatter/serializer/osm/osm_graph.py index 0c495e1..8ea25ee 100644 --- a/src/osm_osw_reformatter/serializer/osm/osm_graph.py +++ b/src/osm_osw_reformatter/serializer/osm/osm_graph.py @@ -614,12 +614,30 @@ def to_geojson(self, *args) -> None: zones_path = args[4] polygons_path = args[5] - _id = 1 + edge_id_counter = 1 + node_id_counter = 1 + point_id_counter = 1 + line_id_counter = 1 + zone_id_counter = 1 + polygon_id_counter = 1 + + def _source_id(node_key): + id_str = str(node_key) + if isinstance(node_key, str) and id_str[:1] in {"p", "l", "z", "g"}: + return id_str[1:] + return id_str + + def _assign_ids(properties, new_id, source_id): + properties["_id"] = str(new_id) + if "ext:osm_id" not in properties: + properties["ext:osm_id"] = str(properties.get("osm_id", source_id)) + properties.pop("osm_id", None) + edge_features = [] for u, v, d in self.G.edges(data=True): d_copy = {**d} - d_copy['_id'] = str(_id) - _id += 1 + d_copy['_id'] = str(edge_id_counter) + edge_id_counter += 1 d_copy['_u_id'] = str(u) d_copy['_v_id'] = str(v) @@ -645,12 +663,11 @@ def to_geojson(self, *args) -> None: polygon_features = [] for n, d in self.G.nodes(data=True): d_copy = {**d} - id_str = str(n) - trimmed_id = id_str[1:] if isinstance(n, str) else id_str - d_copy["_id"] = trimmed_id - d_copy['ext:osm_id'] = str(d_copy.get('osm_id', d_copy["_id"])) + source_id = _source_id(n) if OSWPointNormalizer.osw_point_filter(d): + _assign_ids(d_copy, point_id_counter, source_id) + point_id_counter += 1 geometry = mapping(d_copy.pop("geometry")) if "lon" in d_copy: @@ -663,25 +680,32 @@ def to_geojson(self, *args) -> None: {"type": "Feature", "geometry": geometry, "properties": d_copy} ) elif OSWLineNormalizer.osw_line_filter(d): + _assign_ids(d_copy, line_id_counter, source_id) + line_id_counter += 1 geometry = mapping(d_copy.pop("geometry")) line_features.append( {"type": "Feature", "geometry": geometry, "properties": d_copy} ) elif OSWZoneNormalizer.osw_zone_filter(d): + _assign_ids(d_copy, zone_id_counter, source_id) + zone_id_counter += 1 geometry = mapping(d_copy.pop("geometry")) zone_features.append( {"type": "Feature", "geometry": geometry, "properties": d_copy} ) elif OSWPolygonNormalizer.osw_polygon_filter(d): + _assign_ids(d_copy, polygon_id_counter, source_id) + polygon_id_counter += 1 geometry = mapping(d_copy.pop("geometry")) polygon_features.append( {"type": "Feature", "geometry": geometry, "properties": d_copy} ) else: - d_copy['_id'] = str(n) + _assign_ids(d_copy, node_id_counter, source_id) + node_id_counter += 1 geometry = mapping(d_copy.pop('geometry')) diff --git a/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py b/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py index 4a5114c..cb39aef 100644 --- a/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py +++ b/src/osm_osw_reformatter/serializer/osm/osm_normalizer.py @@ -234,6 +234,9 @@ def process_output(self, osmnodes, osmways, osmrelations): node_id_map = {} way_id_map = {} rel_id_map = {} + next_node_id = 1 + next_way_id = 1 + next_rel_id = 1 def _set_id_tag(osm_obj, new_id): tags = getattr(osm_obj, "tags", None) @@ -260,13 +263,24 @@ def _set_id_tag(osm_obj, new_id): else: tags["_id"] = value + def _member_type(member): + for attr in ("type", "member_type", "objtype", "element_type"): + value = getattr(member, attr, None) + if isinstance(value, str): + normalized = value.lower() + if normalized in ("node", "way", "relation"): + return normalized + return None + # Remap node IDs sequentially starting at 1 for node in osmnodes: old_id = getattr(node, "id", None) if old_id is None: continue - new_id = len(node_id_map) + 1 - node_id_map[old_id] = new_id + new_id = next_node_id + next_node_id += 1 + # Keep first mapping for refs that still point to source IDs. + node_id_map.setdefault(old_id, new_id) node.id = new_id _set_id_tag(node, new_id) @@ -274,8 +288,9 @@ def _set_id_tag(osm_obj, new_id): for way in osmways: old_id = getattr(way, "id", None) if old_id is not None: - new_id = len(way_id_map) + 1 - way_id_map[old_id] = new_id + new_id = next_way_id + next_way_id += 1 + way_id_map.setdefault(old_id, new_id) way.id = new_id _set_id_tag(way, new_id) @@ -285,12 +300,14 @@ def _set_id_tag(osm_obj, new_id): for ref in node_refs: if isinstance(ref, int): if ref not in node_id_map: - new_id = len(node_id_map) + 1 + new_id = next_node_id + next_node_id += 1 node_id_map[ref] = new_id new_refs.append(node_id_map.get(ref, ref)) elif hasattr(ref, "id"): if ref.id not in node_id_map: - new_id = len(node_id_map) + 1 + new_id = next_node_id + next_node_id += 1 node_id_map[ref.id] = new_id ref.id = node_id_map.get(ref.id, ref.id) new_refs.append(ref) @@ -310,8 +327,9 @@ def _set_id_tag(osm_obj, new_id): for rel in osmrelations: old_id = getattr(rel, "id", None) if old_id is not None: - new_id = len(rel_id_map) + 1 - rel_id_map[old_id] = new_id + new_id = next_rel_id + next_rel_id += 1 + rel_id_map.setdefault(old_id, new_id) rel.id = new_id _set_id_tag(rel, new_id) @@ -320,16 +338,57 @@ def _set_id_tag(osm_obj, new_id): if not hasattr(member, "ref"): continue ref = member.ref + m_type = _member_type(member) if isinstance(ref, int): - if ref not in node_id_map and ref not in way_id_map and ref not in rel_id_map: - new_id = len(rel_id_map) + 1 - rel_id_map[ref] = new_id - member.ref = node_id_map.get(ref, way_id_map.get(ref, rel_id_map.get(ref, ref))) + if m_type == "node": + if ref not in node_id_map: + new_id = next_node_id + next_node_id += 1 + node_id_map[ref] = new_id + member.ref = node_id_map.get(ref, ref) + elif m_type == "way": + if ref not in way_id_map: + new_id = next_way_id + next_way_id += 1 + way_id_map[ref] = new_id + member.ref = way_id_map.get(ref, ref) + elif m_type == "relation": + if ref not in rel_id_map: + new_id = next_rel_id + next_rel_id += 1 + rel_id_map[ref] = new_id + member.ref = rel_id_map.get(ref, ref) + else: + if ref not in node_id_map and ref not in way_id_map and ref not in rel_id_map: + new_id = next_rel_id + next_rel_id += 1 + rel_id_map[ref] = new_id + member.ref = node_id_map.get(ref, way_id_map.get(ref, rel_id_map.get(ref, ref))) elif hasattr(ref, "id"): - if ref.id not in node_id_map and ref.id not in way_id_map and ref.id not in rel_id_map: - new_id = len(rel_id_map) + 1 - rel_id_map[ref.id] = new_id - ref.id = node_id_map.get(ref.id, way_id_map.get(ref.id, rel_id_map.get(ref.id, ref.id))) + if m_type == "node": + if ref.id not in node_id_map: + new_id = next_node_id + next_node_id += 1 + node_id_map[ref.id] = new_id + ref.id = node_id_map.get(ref.id, ref.id) + elif m_type == "way": + if ref.id not in way_id_map: + new_id = next_way_id + next_way_id += 1 + way_id_map[ref.id] = new_id + ref.id = way_id_map.get(ref.id, ref.id) + elif m_type == "relation": + if ref.id not in rel_id_map: + new_id = next_rel_id + next_rel_id += 1 + rel_id_map[ref.id] = new_id + ref.id = rel_id_map.get(ref.id, ref.id) + else: + if ref.id not in node_id_map and ref.id not in way_id_map and ref.id not in rel_id_map: + new_id = next_rel_id + next_rel_id += 1 + rel_id_map[ref.id] = new_id + ref.id = node_id_map.get(ref.id, way_id_map.get(ref.id, rel_id_map.get(ref.id, ref.id))) # Ensure deterministic ordering now that IDs have been remapped if hasattr(osmnodes, "sort"): diff --git a/tests/unit_tests/test_files/bug_2942.xml b/tests/unit_tests/test_files/bug_2942.xml new file mode 100644 index 0000000..8d85298 --- /dev/null +++ b/tests/unit_tests/test_files/bug_2942.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit_tests/test_serializer/test_osm_graph.py b/tests/unit_tests/test_serializer/test_osm_graph.py index 8d54635..f6f1b8d 100644 --- a/tests/unit_tests/test_serializer/test_osm_graph.py +++ b/tests/unit_tests/test_serializer/test_osm_graph.py @@ -734,7 +734,7 @@ def test_tagged_node_parser_adds_osw_nodes(self): self.assertIn(123, graph.nodes) self.assertEqual(graph.nodes[123]['barrier'], 'kerb') - def test_to_geojson_node_ids_preserved(self): + def test_to_geojson_nodes_use_sequential_ids_and_preserve_ext_osm_id(self): graph = nx.MultiDiGraph() graph.add_node( 5959268989, @@ -771,9 +771,10 @@ def test_to_geojson_node_ids_preserved(self): self.assertEqual(len(data['features']), 1) props = data['features'][0]['properties'] - self.assertEqual(props['_id'], '5959268989') + self.assertEqual(props['_id'], '1') + self.assertEqual(props['ext:osm_id'], '5959268989') - def test_to_geojson_point_ids_trim_prefix(self): + def test_to_geojson_points_use_sequential_ids_and_preserve_ext_osm_id(self): graph = nx.MultiDiGraph() graph.add_node( 'p123', @@ -807,9 +808,59 @@ def test_to_geojson_point_ids_trim_prefix(self): self.assertEqual(len(data['features']), 1) props = data['features'][0]['properties'] - self.assertEqual(props['_id'], '123') + self.assertEqual(props['_id'], '1') self.assertEqual(props['ext:osm_id'], '123') + def test_to_geojson_polygon_ids_are_sequential_even_with_sparse_source_ids(self): + graph = nx.MultiDiGraph() + graph.add_node( + "g1", + geometry=Polygon([(0, 0), (0, 1), (1, 1), (0, 0)]), + **{"ext:building": "yes"}, + ) + graph.add_node( + "g2", + geometry=Polygon([(1, 0), (1, 1), (2, 1), (1, 0)]), + **{"ext:building": "yes"}, + ) + graph.add_node( + "g4", + geometry=Polygon([(2, 0), (2, 1), (3, 1), (2, 0)]), + **{"ext:building": "yes"}, + ) + graph.add_node( + "g6", + geometry=Polygon([(3, 0), (3, 1), (4, 1), (3, 0)]), + **{"ext:building": "yes"}, + ) + + osm_graph = OSMGraph(G=graph) + + with TemporaryDirectory() as tmpdir: + nodes_path = os.path.join(tmpdir, 'nodes.geojson') + edges_path = os.path.join(tmpdir, 'edges.geojson') + points_path = os.path.join(tmpdir, 'points.geojson') + lines_path = os.path.join(tmpdir, 'lines.geojson') + zones_path = os.path.join(tmpdir, 'zones.geojson') + polygons_path = os.path.join(tmpdir, 'polygons.geojson') + + osm_graph.to_geojson( + nodes_path, + edges_path, + points_path, + lines_path, + zones_path, + polygons_path, + ) + + self.assertTrue(os.path.exists(polygons_path)) + with open(polygons_path) as f: + data = json.load(f) + + ids = [feat["properties"]["_id"] for feat in data["features"]] + self.assertEqual(ids, ["1", "2", "3", "4"]) + self.assertEqual(len(ids), len(set(ids))) + def test_to_undirected_on_simple_graph(self): g = nx.Graph() g.add_edge(1, 2) diff --git a/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py b/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py index b74dd1d..0cc3a05 100644 --- a/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py +++ b/tests/unit_tests/test_serializer/test_osm_osm_normalizer.py @@ -25,8 +25,10 @@ def __init__(self, tags=None, osm_id=-1): class DummyMember: - def __init__(self, ref): + def __init__(self, ref, member_type=None): self.ref = ref + if member_type is not None: + self.type = member_type class DummyRel: @@ -205,6 +207,45 @@ def test_process_output_handles_various_refs_and_attrs(self): self.assertIsInstance(r, str) self.assertGreaterEqual(rel.members[0].ref, 0) + def test_process_output_assigns_unique_ids_when_source_ids_repeat(self): + nodes = [ + DummyOsmGeometry(tags={'_id': ['10']}, osm_id=10), + DummyOsmGeometry(tags={'_id': ['10']}, osm_id=10), + DummyOsmGeometry(tags={'_id': ['11']}, osm_id=11), + ] + ways = [ + DummyOsmGeometry(tags={'_id': ['20']}, osm_id=20), + DummyOsmGeometry(tags={'_id': ['20']}, osm_id=20), + DummyOsmGeometry(tags={'_id': ['21']}, osm_id=21), + ] + + self.normalizer.process_output(nodes, ways, []) + + self.assertEqual(sorted([n.id for n in nodes]), [1, 2, 3]) + self.assertEqual(sorted([w.id for w in ways]), [1, 2, 3]) + for idx, node in enumerate(sorted(nodes, key=lambda n: n.id), start=1): + self.assertEqual(node.tags.get('_id'), [str(idx)]) + for idx, way in enumerate(sorted(ways, key=lambda w: w.id), start=1): + self.assertEqual(way.tags.get('_id'), [str(idx)]) + + def test_process_output_relation_member_type_uses_correct_id_map(self): + node = DummyOsmGeometry(tags={'_id': ['7']}, osm_id=7) + way = DummyOsmGeometry(tags={'_id': ['7']}, osm_id=7) + rel = DummyRel( + 9, + members=[ + DummyMember(ref=7, member_type='node'), + DummyMember(ref=7, member_type='way'), + ], + ) + + self.normalizer.process_output([node], [way], [rel]) + + self.assertEqual(node.id, 1) + self.assertEqual(way.id, 1) + self.assertEqual(rel.members[0].ref, 1) + self.assertEqual(rel.members[1].ref, 1) + if __name__ == '__main__': unittest.main() From 9c4c6389ce51c9b6ad8b3b6fa914d64f5e6b23ed Mon Sep 17 00:00:00 2001 From: Suresh Devalapalli Date: Tue, 24 Feb 2026 15:13:47 -0800 Subject: [PATCH 2/3] fixes for missing/unmpaped _u_id, _v_id, and _w_id --- .../serializer/osm/osm_graph.py | 66 ++++++++++------ .../test_serializer/test_osm_graph.py | 79 +++++++++++++++++++ 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/osm_osw_reformatter/serializer/osm/osm_graph.py b/src/osm_osw_reformatter/serializer/osm/osm_graph.py index 8ea25ee..ced5893 100644 --- a/src/osm_osw_reformatter/serializer/osm/osm_graph.py +++ b/src/osm_osw_reformatter/serializer/osm/osm_graph.py @@ -633,34 +633,23 @@ def _assign_ids(properties, new_id, source_id): properties["ext:osm_id"] = str(properties.get("osm_id", source_id)) properties.pop("osm_id", None) - edge_features = [] - for u, v, d in self.G.edges(data=True): - d_copy = {**d} - d_copy['_id'] = str(edge_id_counter) - edge_id_counter += 1 - d_copy['_u_id'] = str(u) - d_copy['_v_id'] = str(v) - - d_copy['ext:osm_id'] = str(d['osm_id']) - - if 'osm_id' in d_copy: - d_copy.pop('osm_id') - - if 'segment' in d_copy: - d_copy.pop('segment') - - geometry = mapping(d_copy.pop('geometry')) - - edge_features.append( - {'type': 'Feature', 'geometry': geometry, 'properties': d_copy} - ) - edges_fc = {**OSW_JSON_HEADER, **{"features": edge_features}} + def _remap_node_ref(ref, node_id_map): + if ref in node_id_map: + return node_id_map[ref] + try: + ref_int = int(ref) + except (TypeError, ValueError): + ref_int = None + if ref_int is not None and ref_int in node_id_map: + return node_id_map[ref_int] + return str(ref) node_features = [] point_features = [] line_features = [] zone_features = [] polygon_features = [] + node_id_map = {} for n, d in self.G.nodes(data=True): d_copy = {**d} source_id = _source_id(n) @@ -705,6 +694,7 @@ def _assign_ids(properties, new_id, source_id): ) else: _assign_ids(d_copy, node_id_counter, source_id) + node_id_map[n] = d_copy["_id"] node_id_counter += 1 geometry = mapping(d_copy.pop('geometry')) @@ -718,6 +708,38 @@ def _assign_ids(properties, new_id, source_id): node_features.append( {'type': 'Feature', 'geometry': geometry, 'properties': d_copy} ) + + for zone_feature in zone_features: + props = zone_feature.get("properties", {}) + w_ids = props.get("_w_id") + if isinstance(w_ids, list): + props["_w_id"] = [str(_remap_node_ref(ref, node_id_map)) for ref in w_ids] + elif w_ids is not None: + props["_w_id"] = str(_remap_node_ref(w_ids, node_id_map)) + + edge_features = [] + for u, v, d in self.G.edges(data=True): + d_copy = {**d} + d_copy['_id'] = str(edge_id_counter) + edge_id_counter += 1 + d_copy['_u_id'] = str(node_id_map.get(u, u)) + d_copy['_v_id'] = str(node_id_map.get(v, v)) + + d_copy['ext:osm_id'] = str(d['osm_id']) + + if 'osm_id' in d_copy: + d_copy.pop('osm_id') + + if 'segment' in d_copy: + d_copy.pop('segment') + + geometry = mapping(d_copy.pop('geometry')) + + edge_features.append( + {'type': 'Feature', 'geometry': geometry, 'properties': d_copy} + ) + edges_fc = {**OSW_JSON_HEADER, **{"features": edge_features}} + nodes_fc = {**OSW_JSON_HEADER, **{"features": node_features}} points_fc = {**OSW_JSON_HEADER, **{"features": point_features}} lines_fc = {**OSW_JSON_HEADER, **{"features": line_features}} diff --git a/tests/unit_tests/test_serializer/test_osm_graph.py b/tests/unit_tests/test_serializer/test_osm_graph.py index f6f1b8d..fcff537 100644 --- a/tests/unit_tests/test_serializer/test_osm_graph.py +++ b/tests/unit_tests/test_serializer/test_osm_graph.py @@ -861,6 +861,85 @@ def test_to_geojson_polygon_ids_are_sequential_even_with_sparse_source_ids(self) self.assertEqual(ids, ["1", "2", "3", "4"]) self.assertEqual(len(ids), len(set(ids))) + def test_to_geojson_edges_reference_remapped_node_ids(self): + graph = nx.MultiDiGraph() + graph.add_node(10, geometry=Point(0, 0), lon=0.0, lat=0.0) + graph.add_node(20, geometry=Point(1, 1), lon=1.0, lat=1.0) + graph.add_edge( + 10, + 20, + geometry=LineString([(0, 0), (1, 1)]), + osm_id="99", + ) + + osm_graph = OSMGraph(G=graph) + + with TemporaryDirectory() as tmpdir: + nodes_path = os.path.join(tmpdir, 'nodes.geojson') + edges_path = os.path.join(tmpdir, 'edges.geojson') + points_path = os.path.join(tmpdir, 'points.geojson') + lines_path = os.path.join(tmpdir, 'lines.geojson') + zones_path = os.path.join(tmpdir, 'zones.geojson') + polygons_path = os.path.join(tmpdir, 'polygons.geojson') + + osm_graph.to_geojson( + nodes_path, + edges_path, + points_path, + lines_path, + zones_path, + polygons_path, + ) + + with open(nodes_path) as f: + node_data = json.load(f) + with open(edges_path) as f: + edge_data = json.load(f) + + node_ids = {feat["properties"]["_id"] for feat in node_data["features"]} + edge = edge_data["features"][0]["properties"] + self.assertIn(edge["_u_id"], node_ids) + self.assertIn(edge["_v_id"], node_ids) + + def test_to_geojson_zones_reference_remapped_node_ids_in_w_id(self): + graph = nx.MultiDiGraph() + graph.add_node(10, geometry=Point(0, 0), lon=0.0, lat=0.0) + graph.add_node(20, geometry=Point(1, 0), lon=1.0, lat=0.0) + graph.add_node( + "z100", + geometry=Polygon([(0, 0), (1, 0), (0, 1), (0, 0)]), + highway="pedestrian", + _w_id=["10", "20"], + ) + + osm_graph = OSMGraph(G=graph) + + with TemporaryDirectory() as tmpdir: + nodes_path = os.path.join(tmpdir, 'nodes.geojson') + edges_path = os.path.join(tmpdir, 'edges.geojson') + points_path = os.path.join(tmpdir, 'points.geojson') + lines_path = os.path.join(tmpdir, 'lines.geojson') + zones_path = os.path.join(tmpdir, 'zones.geojson') + polygons_path = os.path.join(tmpdir, 'polygons.geojson') + + osm_graph.to_geojson( + nodes_path, + edges_path, + points_path, + lines_path, + zones_path, + polygons_path, + ) + + with open(nodes_path) as f: + node_data = json.load(f) + with open(zones_path) as f: + zone_data = json.load(f) + + node_ids = {feat["properties"]["_id"] for feat in node_data["features"]} + zone_w_ids = zone_data["features"][0]["properties"]["_w_id"] + self.assertTrue(all(wid in node_ids for wid in zone_w_ids)) + def test_to_undirected_on_simple_graph(self): g = nx.Graph() g.add_edge(1, 2) From 997560666672cf8a4842857c8c6300a933d93efe Mon Sep 17 00:00:00 2001 From: sujata-m Date: Tue, 3 Mar 2026 13:37:45 +0530 Subject: [PATCH 3/3] updated changelog and version --- CHANGELOG.md | 5 +++++ src/osm_osw_reformatter/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e1e26..0a6231f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change log +### 0.3.2 +- Fix duplicate polygon `_id` generation in OSM→OSW export by assigning sequential IDs per feature type. +- Remap edge `_u_id`/`_v_id` and zone `_w_id` references to exported node IDs so references stay consistent after ID normalization. +- Harden OSM ID remapping in normalizer output with deterministic per-type counters and relation-member type-aware reference rewrites. + ### 0.3.1 - Preserve custom `ext:*` features across all geometries: ext-only points keep numeric IDs (no `p` prefix), ext-only lines/polygons are retained, and custom attributes are emitted in the appropriate GeoJSON file. - Add schema-safe handling for ext-only geometries during construction to avoid missing-ref crashes. diff --git a/src/osm_osw_reformatter/version.py b/src/osm_osw_reformatter/version.py index e1424ed..73e3bb4 100644 --- a/src/osm_osw_reformatter/version.py +++ b/src/osm_osw_reformatter/version.py @@ -1 +1 @@ -__version__ = '0.3.1' +__version__ = '0.3.2'