diff --git a/docs/plots/plots.py b/docs/plots/plots.py
index 796d4cfe..18010a86 100644
--- a/docs/plots/plots.py
+++ b/docs/plots/plots.py
@@ -94,7 +94,7 @@
 buffer = 1250
 poly_wgs, _ = io.buffered_point_poly(lng, lat, buffer)
 graph_raw = io.osm_graph_from_poly(poly_wgs, simplify=False)
-graph_utm = io.osm_graph_from_poly(poly_wgs, simplify=True, iron_edges=True)
+graph_utm = io.osm_graph_from_poly(poly_wgs, simplify=True)
 # plot buffer
 buffered_point, _ = io.buffered_point_poly(lng, lat, 750, projected=True)
 min_x, min_y, max_x, max_y = buffered_point.bounds
@@ -116,25 +116,8 @@ def simple_plot(_G, _path, plot_geoms=True):
     )
 
 
-simple_plot(graph_raw, f"{IMAGES_PATH}/graph_cleaning_1.{FORMAT}", plot_geoms=False)
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_1b.{FORMAT}")
-
-graph_utm = graphs.nx_simple_geoms(graph_raw)
-graph_utm = graphs.nx_remove_filler_nodes(graph_utm)
-graph_utm = graphs.nx_remove_dangling_nodes(graph_utm, despine=15, remove_disconnected=10)
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_2.{FORMAT}")
-# first pass of consolidation
-graph_utm = graphs.nx_consolidate_nodes(graph_utm, buffer_dist=15, crawl=True)
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_3.{FORMAT}")
-# split opposing line geoms to facilitate parallel merging
-graph_utm = graphs.nx_split_opposing_geoms(graph_utm, buffer_dist=15)
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_4.{FORMAT}")
-# second pass of consolidation
-graph_utm = graphs.nx_consolidate_nodes(graph_utm, buffer_dist=15, crawl=False, neighbour_policy="indirect")
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_5.{FORMAT}")
-# iron edges
-graph_utm = graphs.nx_iron_edges(graph_utm)
-simple_plot(graph_utm, f"{IMAGES_PATH}/graph_cleaning_6.{FORMAT}")
+simple_plot(graph_raw, f"{IMAGES_PATH}/graph_raw.{FORMAT}")
+simple_plot(graph_utm, f"{IMAGES_PATH}/graph_clean.{FORMAT}")
 # LAYERS MODULE
 # show assignment to network
 # random seed 25
diff --git a/docs/public/images/assignment.png b/docs/public/images/assignment.png
index 851839cf..4db50d53 100644
Binary files a/docs/public/images/assignment.png and b/docs/public/images/assignment.png differ
diff --git a/docs/public/images/assignment_decomposed.png b/docs/public/images/assignment_decomposed.png
index e024b9c9..f1c1246d 100644
Binary files a/docs/public/images/assignment_decomposed.png and b/docs/public/images/assignment_decomposed.png differ
diff --git a/docs/public/images/assignment_plot.png b/docs/public/images/assignment_plot.png
index 18289759..ce1fb86b 100644
Binary files a/docs/public/images/assignment_plot.png and b/docs/public/images/assignment_plot.png differ
diff --git a/docs/public/images/betas.png b/docs/public/images/betas.png
index 920100e6..c3c94215 100644
Binary files a/docs/public/images/betas.png and b/docs/public/images/betas.png differ
diff --git a/docs/public/images/graph.png b/docs/public/images/graph.png
index 7554c8b9..fbf299ca 100644
Binary files a/docs/public/images/graph.png and b/docs/public/images/graph.png differ
diff --git a/docs/public/images/graph_clean.png b/docs/public/images/graph_clean.png
new file mode 100644
index 00000000..2db97eae
Binary files /dev/null and b/docs/public/images/graph_clean.png differ
diff --git a/docs/public/images/graph_colour.png b/docs/public/images/graph_colour.png
index 1018d337..ee632de8 100644
Binary files a/docs/public/images/graph_colour.png and b/docs/public/images/graph_colour.png differ
diff --git a/docs/public/images/graph_decomposed.png b/docs/public/images/graph_decomposed.png
index d24853df..1a3fbc28 100644
Binary files a/docs/public/images/graph_decomposed.png and b/docs/public/images/graph_decomposed.png differ
diff --git a/docs/public/images/graph_dual.png b/docs/public/images/graph_dual.png
index 01ae5a1f..67cfee01 100644
Binary files a/docs/public/images/graph_dual.png and b/docs/public/images/graph_dual.png differ
diff --git a/docs/public/images/graph_example.png b/docs/public/images/graph_example.png
index c8f0775e..11ac6a85 100644
Binary files a/docs/public/images/graph_example.png and b/docs/public/images/graph_example.png differ
diff --git a/docs/public/images/graph_raw.png b/docs/public/images/graph_raw.png
new file mode 100644
index 00000000..c6bf43a0
Binary files /dev/null and b/docs/public/images/graph_raw.png differ
diff --git a/docs/public/images/graph_simple.png b/docs/public/images/graph_simple.png
index 503136d8..da0ea8f9 100644
Binary files a/docs/public/images/graph_simple.png and b/docs/public/images/graph_simple.png differ
diff --git a/docs/public/images/intro_mixed_uses.png b/docs/public/images/intro_mixed_uses.png
index b70bd693..26fd801f 100644
Binary files a/docs/public/images/intro_mixed_uses.png and b/docs/public/images/intro_mixed_uses.png differ
diff --git a/docs/public/images/intro_segment_harmonic.png b/docs/public/images/intro_segment_harmonic.png
index a557c54b..6202c6df 100644
Binary files a/docs/public/images/intro_segment_harmonic.png and b/docs/public/images/intro_segment_harmonic.png differ
diff --git a/docs/src/pages/rustalgos/rustalgos.md b/docs/src/pages/rustalgos/rustalgos.md
index e66b1623..c50ecb73 100644
--- a/docs/src/pages/rustalgos/rustalgos.md
+++ b/docs/src/pages/rustalgos/rustalgos.md
@@ -535,7 +535,7 @@ Overriding the default $w_{min}$ will adjust the $d_{max}$ accordingly, for exam
   </div>
   <span class="pt">)-&gt;[</span>
   <span class="pr">list[int]</span>
-  <span class="pr">list[float</span>
+  <span class="pr">list[float]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -1903,7 +1903,7 @@ datapoints are not located with high spatial precision.
   </div>
   <span class="pt">)-&gt;[</span>
   <span class="pr">list[int]</span>
-  <span class="pr">NodeVisit</span>
+  <span class="pr">NodeVisit]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -1940,7 +1940,7 @@ datapoints are not located with high spatial precision.
   </div>
   <span class="pt">)-&gt;[</span>
   <span class="pr">list[int]</span>
-  <span class="pr">NodeVisit</span>
+  <span class="pr">NodeVisit]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -1979,7 +1979,7 @@ datapoints are not located with high spatial precision.
   <span class="pr">list[int]</span>
   <span class="pr">list[int]</span>
   <span class="pr">NodeVisit]</span>
-  <span class="pr">EdgeVisit</span>
+  <span class="pr">EdgeVisit]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -2807,22 +2807,22 @@ datapoints are not located with high spatial precision.
 
  
 
-<span class="name">node_xys</span><span class="annotation">: list[tuple[float, float]]</span>
+<span class="name">node_ys</span><span class="annotation">: list[float]</span>
 
 
  
 
-<span class="name">node_xs</span><span class="annotation">: list[float]</span>
+<span class="name">node_lives</span><span class="annotation">: list[bool]</span>
 
 
  
 
-<span class="name">node_ys</span><span class="annotation">: list[float]</span>
+<span class="name">node_xs</span><span class="annotation">: list[float]</span>
 
 
  
 
-<span class="name">node_lives</span><span class="annotation">: list[bool]</span>
+<span class="name">node_xys</span><span class="annotation">: list[tuple[float, float]]</span>
 
 
  
@@ -2999,7 +2999,6 @@ datapoints are not located with high spatial precision.
     <span class="pa"> bool = False</span>
   </div>
   <span class="pt">)-&gt;[</span>
-  <span class="pr">Union[Buffer</span>
   <span class="pr">Any]]</span>
   <span class="pr">Any]]]</span>
   <span class="pr">bool</span>
@@ -3009,7 +3008,6 @@ datapoints are not located with high spatial precision.
   <span class="pr">str</span>
   <span class="pr">bytes</span>
   <span class="pr">_NestedSequence[bool | int | float | complex | str | bytes]]</span>
-  <span class="pr">Union[Buffer</span>
   <span class="pr">Any]]</span>
   <span class="pr">Any]]]</span>
   <span class="pr">bool</span>
@@ -3019,7 +3017,6 @@ datapoints are not located with high spatial precision.
   <span class="pr">str</span>
   <span class="pr">bytes</span>
   <span class="pr">_NestedSequence[bool | int | float | complex | str | bytes]]</span>
-  <span class="pr">Union[Buffer</span>
   <span class="pr">Any]]</span>
   <span class="pr">Any]]]</span>
   <span class="pr">bool</span>
@@ -3028,7 +3025,7 @@ datapoints are not located with high spatial precision.
   <span class="pr">complex</span>
   <span class="pr">str</span>
   <span class="pr">bytes</span>
-  <span class="pr">_NestedSequence[bool | int | float | complex | str | bytes</span>
+  <span class="pr">_NestedSequence[bool | int | float | complex | str | bytes]]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -3072,7 +3069,6 @@ datapoints are not located with high spatial precision.
     <span class="pa"> bool = False</span>
   </div>
   <span class="pt">)-&gt;[</span>
-  <span class="pr">Union[Buffer</span>
   <span class="pr">dtype[Any]]</span>
   <span class="pr">dtype[Any]]]</span>
   <span class="pr">bool</span>
diff --git a/docs/src/pages/tools/graphs.md b/docs/src/pages/tools/graphs.md
index 37c5ea66..4b5eaabc 100644
--- a/docs/src/pages/tools/graphs.md
+++ b/docs/src/pages/tools/graphs.md
@@ -331,11 +331,21 @@ side-effects as a function of varied node intensities when computing network cen
 
 
 <div class="content">
-<span class="name">nx_iron_edges</span><div class="signature">
+<span class="name">nx_iron_edges</span><div class="signature multiline">
   <span class="pt">(</span>
   <div class="param">
     <span class="pn">nx_multigraph</span>
   </div>
+  <div class="param">
+    <span class="pn">simplify_by_angle</span>
+    <span class="pc">:</span>
+    <span class="pa"> int = 100</span>
+  </div>
+  <div class="param">
+    <span class="pn">min_self_loop_length</span>
+    <span class="pc">:</span>
+    <span class="pa"> int = 100</span>
+  </div>
   <span class="pt">)</span>
 </div>
 </div>
@@ -353,6 +363,26 @@ side-effects as a function of varied node intensities when computing network cen
  A `networkX` `MultiGraph` in a projected coordinate system, containing `x` and `y` node attributes, and `geom` edge attributes containing `LineString` geoms.</div>
 </div>
 
+<div class="param-set">
+  <div class="def">
+    <div class="name">simplify_by_angle</div>
+    <div class="type">int</div>
+  </div>
+  <div class="desc">
+
+ The maximum angle to permit for a given edge. Angles greater than this will be reduced.</div>
+</div>
+
+<div class="param-set">
+  <div class="def">
+    <div class="name">min_self_loop_length</div>
+    <div class="type">int</div>
+  </div>
+  <div class="desc">
+
+ Maximum self loop length to permit for a given edge.</div>
+</div>
+
 ### Returns
 <div class="param-set">
   <div class="def">
@@ -578,9 +608,9 @@ side-effects as a function of varied node intensities when computing network cen
 
  See the guide on [graph cleaning](/guide#graph-cleaning) for more information.
 
-![Example raw graph from OSM](/images/graph_cleaning_1.png) _The pre-consolidation OSM street network for Soho, London. © OpenStreetMap contributors._
+![Example raw graph from OSM](/images/graph_raw.png) _The pre-consolidation OSM street network for Soho, London. © OpenStreetMap contributors._
 
-![Example cleaned graph](/images/graph_cleaning_5.png) _The consolidated OSM street network for Soho, London. © OpenStreetMap contributors._
+![Example cleaned graph](/images/graph_clean.png) _The consolidated OSM street network for Soho, London. © OpenStreetMap contributors._
 
 </div>
 
diff --git a/docs/src/pages/tools/util.md b/docs/src/pages/tools/util.md
index 3333dd6a..e211496a 100644
--- a/docs/src/pages/tools/util.md
+++ b/docs/src/pages/tools/util.md
@@ -664,6 +664,7 @@ layout: ../../layouts/PageLayout.astro
   <span class="pt">)-&gt;[</span>
   <span class="pr">STRtree</span>
   <span class="pr">list[dict[str</span>
+  <span class="pr">Any]]</span>
   <span class="pt">]</span>
 </div>
 </div>
@@ -688,6 +689,7 @@ layout: ../../layouts/PageLayout.astro
   <span class="pt">)-&gt;[</span>
   <span class="pr">STRtree</span>
   <span class="pr">list[dict[str</span>
+  <span class="pr">Any]]</span>
   <span class="pt">]</span>
 </div>
 </div>
diff --git a/pyproject.toml b/pyproject.toml
index 1abc250a..745c5449 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "cityseer"
-version = '4.16.4'
+version = '4.16.5'
 description = "Computational tools for network-based pedestrian-scale urban analysis"
 readme = "README.md"
 requires-python = ">=3.10, <3.14"
diff --git a/pysrc/cityseer/tools/graphs.py b/pysrc/cityseer/tools/graphs.py
index c29ed965..6b73bfe8 100644
--- a/pysrc/cityseer/tools/graphs.py
+++ b/pysrc/cityseer/tools/graphs.py
@@ -515,7 +515,7 @@ def nx_snap_endpoints(nx_multigraph: MultiGraph) -> MultiGraph:
 
 
 def nx_iron_edges(
-    nx_multigraph: MultiGraph,
+    nx_multigraph: MultiGraph, simplify_by_angle: int = 100, min_self_loop_length: int = 100
 ) -> MultiGraph:
     """
     Simplifies edges.
@@ -525,6 +525,10 @@ def nx_iron_edges(
     nx_multigraph: MultiGraph
         A `networkX` `MultiGraph` in a projected coordinate system, containing `x` and `y` node attributes, and `geom`
         edge attributes containing `LineString` geoms.
+    simplify_by_angle: int
+        The maximum angle to permit for a given edge. Angles greater than this will be reduced.
+    min_self_loop_length: int
+        Maximum self loop length to permit for a given edge.
 
     Returns
     -------
@@ -542,15 +546,16 @@ def nx_iron_edges(
     for start_nd_key, end_nd_key, edge_idx, edge_data in tqdm(
         g_multi_copy.edges(keys=True, data=True), disable=config.QUIET_MODE
     ):
+        edge_geom: geometry.LineString = edge_data["geom"]
         # only apply to non looping geoms otherwise issues occur
-        if start_nd_key == end_nd_key:
+        if start_nd_key == end_nd_key and edge_geom.length < min_self_loop_length:
+            remove_edges.append((start_nd_key, end_nd_key, edge_idx))
             continue
-        edge_geom: geometry.LineString = edge_data["geom"]
         hits = edges_tree.query(edge_geom, predicate="crosses")
         if len(hits):
             remove_edges.append((start_nd_key, end_nd_key, edge_idx))
             continue
-        line_coords = simplify_line_by_angle(edge_geom.coords, 100)
+        line_coords = simplify_line_by_angle(edge_geom.coords, simplify_by_angle)
         g_multi_copy[start_nd_key][end_nd_key][edge_idx]["geom"] = geometry.LineString(line_coords)
     g_multi_copy.remove_edges_from(remove_edges)
     # straightening parallel edges can create duplicates
@@ -746,6 +751,9 @@ def _squash_adjacent(
                 new_edge_geom = geometry.LineString(line_coords)
                 if new_edge_geom.length == 0:
                     continue
+                # bail if short self loop
+                if new_nd_name == target_nd_key and new_edge_geom.length < 100:
+                    continue
                 # check that a duplicate is not being added
                 dupe = False
                 if nx_multigraph.has_edge(new_nd_name, target_nd_key):
@@ -856,10 +864,10 @@ def nx_consolidate_nodes(
     --------
     See the guide on [graph cleaning](/guide#graph-cleaning) for more information.
 
-    ![Example raw graph from OSM](/images/graph_cleaning_1.png)
+    ![Example raw graph from OSM](/images/graph_raw.png)
     _The pre-consolidation OSM street network for Soho, London. © OpenStreetMap contributors._
 
-    ![Example cleaned graph](/images/graph_cleaning_5.png)
+    ![Example cleaned graph](/images/graph_clean.png)
     _The consolidated OSM street network for Soho, London. © OpenStreetMap contributors._
 
     """
diff --git a/pysrc/cityseer/tools/io.py b/pysrc/cityseer/tools/io.py
index 83b4b02c..4fce9901 100644
--- a/pysrc/cityseer/tools/io.py
+++ b/pysrc/cityseer/tools/io.py
@@ -441,8 +441,8 @@ def osm_graph_from_poly(
         graph_crs = graphs.nx_remove_dangling_nodes(graph_crs, despine=0, remove_disconnected=remove_disconnected)
         # clean by highway types - leave motorway and trunk as is
         for dist, tags, simplify_line_angles in (
-            (20, ["primary"], 45),  # , "primary_link"
-            (18, ["primary", "secondary"], 45),  # , "secondary_link"
+            (24, ["primary"], 45),  # , "primary_link"
+            (20, ["primary", "secondary"], 45),  # , "secondary_link"
             (16, ["primary", "secondary", "tertiary"], 45),  # , "tertiary_link"
         ):
             graph_crs = graphs.nx_split_opposing_geoms(
@@ -453,20 +453,22 @@ def osm_graph_from_poly(
                 osm_hwy_target_tags=tags,
                 prioritise_by_hwy_tag=True,
                 simplify_line_angles=simplify_line_angles,
+                contains_buffer_dist=50,
             )
         for dist, tags, simplify_line_angles in (
-            (20, ["primary"], 95),  # , "primary_link"
-            (18, ["primary", "secondary"], 95),  # , "secondary_link"
+            (24, ["primary"], 95),  # , "primary_link"
+            (20, ["primary", "secondary"], 95),  # , "secondary_link"
             (16, ["primary", "secondary", "tertiary"], 95),  # , "tertiary_link"
         ):
             graph_crs = graphs.nx_consolidate_nodes(
                 graph_crs,
                 buffer_dist=dist,
-                crawl=True,
+                crawl=False,
                 centroid_by_itx=True,
                 osm_hwy_target_tags=tags,
                 prioritise_by_hwy_tag=True,
                 simplify_line_angles=simplify_line_angles,
+                contains_buffer_dist=50,
             )
             graph_crs = graphs.nx_remove_filler_nodes(graph_crs)
         # do smaller scale cleaning
@@ -479,6 +481,7 @@ def osm_graph_from_poly(
             "cycleway",
             "bridleway",
             "footway",
+            "footway_pedestrian",  # plazas
             "path",
             "living_street",
             "unclassified",
@@ -495,6 +498,7 @@ def osm_graph_from_poly(
                 osm_hwy_target_tags=tags,
                 prioritise_by_hwy_tag=True,
                 simplify_line_angles=simplify_angles,
+                contains_buffer_dist=50,
             )
             graph_crs = graphs.nx_consolidate_nodes(
                 graph_crs,
@@ -504,6 +508,7 @@ def osm_graph_from_poly(
                 osm_hwy_target_tags=tags,
                 prioritise_by_hwy_tag=True,
                 simplify_line_angles=simplify_angles,
+                contains_buffer_dist=50,
             )
         graph_crs = graphs.nx_remove_filler_nodes(graph_crs)
         # snap gapped endings - don't clean danglers before this
@@ -517,6 +522,7 @@ def osm_graph_from_poly(
                 "cycleway",
                 "bridleway",
                 "footway",
+                "footway_pedestrian",  # plazas
                 "path",
                 "living_street",
                 "unclassified",
@@ -532,7 +538,7 @@ def osm_graph_from_poly(
             squash_nodes=False,
         )
         # remove longer danglers
-        graph_crs = graphs.nx_remove_dangling_nodes(graph_crs, despine=20)
+        graph_crs = graphs.nx_remove_dangling_nodes(graph_crs, despine=50)
 
     return graph_crs