Simplify trait definition (#605)

diff --git a/Cargo.toml b/Cargo.toml
index 503db1e..24a1bc6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,7 +39,7 @@
 petgraph-core = { path = "crates/core", default-features = false }
 petgraph-graph = { path = "crates/graph", default-features = false }
 petgraph-adjacency-matrix = { path = "crates/adjacency-matrix", default-features = false }
-petgraph-graphmap = { path = "crates/graphmap", default-features = false }
+petgraph-entry = { path = "crates/entry", default-features = false }
 petgraph-csr = { path = "crates/csr", default-features = false }
 petgraph-matrix-graph = { path = "crates/matrix-graph", default-features = false }
 petgraph-algorithms = { path = "crates/algorithms", default-features = false }
@@ -61,19 +61,18 @@
 petgraph-generators = { workspace = true, default-features = false, optional = true }
 petgraph-io = { workspace = true, default-features = false, optional = true }
 
-[dependencies.petgraph-graphmap]
+[dependencies.petgraph-entry]
 workspace = true
-features = ["convert"]
 optional = true
 default-features = false
 
 [features]
 adjacency-matrix = ["dep:petgraph-adjacency-matrix", "petgraph-algorithms/remove-me-only-intended-for-move-adjacency-matrix"]
 csr = ["dep:petgraph-csr"]
-default = ["std", "graphmap", "stable-graph", "matrix-graph", "csr", "adjacency-matrix"]
-graphmap = ["dep:petgraph-graphmap"]
+default = ["std", "entry", "stable-graph", "matrix-graph", "csr", "adjacency-matrix"]
+entry = ["dep:petgraph-entry"]
 matrix-graph = ["dep:petgraph-matrix-graph"]
-serde = ["petgraph-graph/serde", "petgraph-graphmap?/serde"]
+serde = ["petgraph-graph/serde"]
 stable-graph = ["petgraph-graph/stable"]
 io = ["petgraph-io"]
 unstable-generators = ["petgraph-generators"]
@@ -81,11 +80,10 @@
     "petgraph-core/std",
     "petgraph-graph/std",
     "petgraph-adjacency-matrix?/std",
-    "petgraph-graphmap?/std",
     "petgraph-csr?/std",
     "petgraph-io?/std"
 ]
-proptest = ["petgraph-graph/proptest", "petgraph-graphmap?/proptest"]
+proptest = ["petgraph-graph/proptest"]
 
 [lib]
 bench = false
diff --git a/crates/algorithms/benches/shortest_paths/large.rs b/crates/algorithms/benches/shortest_paths/large.rs
index 18a31ef..15725da 100644
--- a/crates/algorithms/benches/shortest_paths/large.rs
+++ b/crates/algorithms/benches/shortest_paths/large.rs
@@ -9,7 +9,8 @@
 
 use criterion::{criterion_group, BenchmarkId, Criterion};
 use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestDistance};
-use petgraph_dino::{DiDinoGraph, NodeId};
+use petgraph_core::node::NodeId;
+use petgraph_dino::DiDinoGraph;
 
 fn get_cargo_workspace() -> Arc<Path> {
     static WORKSPACES: Mutex<BTreeMap<String, Arc<Path>>> = Mutex::new(BTreeMap::new());
diff --git a/crates/algorithms/src/shortest_paths/astar/impl.rs b/crates/algorithms/src/shortest_paths/astar/impl.rs
index a8d1869..cf79bfc 100644
--- a/crates/algorithms/src/shortest_paths/astar/impl.rs
+++ b/crates/algorithms/src/shortest_paths/astar/impl.rs
@@ -3,7 +3,11 @@
 use error_stack::{Report, Result};
 use numi::num::{identity::Zero, ops::AddRef};
 use petgraph_core::{
-    id::{AssociativeGraphId, AttributeMapper},
+    node::NodeId,
+    storage::{
+        auxiliary::{FrequencyHint, Hints, OccupancyHint, PerformanceHint, SecondaryGraphStorage},
+        AuxiliaryGraphStorage,
+    },
     Graph, GraphStorage, Node,
 };
 
@@ -22,7 +26,6 @@
 pub(super) struct AStarImpl<'graph: 'parent, 'parent, S, E, H, C>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: Ord,
 {
@@ -38,15 +41,14 @@
 
     predecessor_mode: PredecessorMode,
 
-    distances: <S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'graph, E::Value>,
-    estimates: <S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'graph, E::Value>,
-    predecessors: <S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'graph, Option<S::NodeId>>,
+    distances: S::SecondaryNodeStorage<'graph, E::Value>,
+    estimates: S::SecondaryNodeStorage<'graph, E::Value>,
+    predecessors: S::SecondaryNodeStorage<'graph, Option<NodeId>>,
 }
 
 impl<'graph: 'parent, 'parent, S, E, H, C> AStarImpl<'graph, 'parent, S, E, H, C>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: AStarMeasure,
     H: GraphHeuristic<S, Value = E::Value>,
@@ -59,8 +61,8 @@
         heuristic: &'parent H,
         connections: C,
 
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
 
         predecessor_mode: PredecessorMode,
     ) -> Result<Self, AStarError> {
@@ -79,13 +81,30 @@
 
         queue.push(source_node.id(), estimate.clone().into_owned());
 
-        let mut distances = <S::NodeId as AssociativeGraphId<S>>::attribute_mapper(graph.storage());
+        let mut distances = graph.storage().secondary_node_storage(Hints {
+            performance: PerformanceHint {
+                read: FrequencyHint::Frequent,
+                write: FrequencyHint::Frequent,
+            },
+            occupancy: OccupancyHint::Dense,
+        });
         distances.set(source, E::Value::zero());
 
-        let estimates = <S::NodeId as AssociativeGraphId<S>>::attribute_mapper(graph.storage());
+        let estimates = graph.storage().secondary_node_storage(Hints {
+            performance: PerformanceHint {
+                read: FrequencyHint::Frequent,
+                write: FrequencyHint::Infrequent,
+            },
+            occupancy: OccupancyHint::Dense,
+        });
 
-        let mut predecessors =
-            <S::NodeId as AssociativeGraphId<S>>::attribute_mapper(graph.storage());
+        let mut predecessors = graph.storage().secondary_node_storage(Hints {
+            performance: PerformanceHint {
+                read: FrequencyHint::Infrequent,
+                write: FrequencyHint::Frequent,
+            },
+            occupancy: OccupancyHint::Dense,
+        });
         if predecessor_mode == PredecessorMode::Record {
             predecessors.set(source, None);
         }
diff --git a/crates/algorithms/src/shortest_paths/astar/mod.rs b/crates/algorithms/src/shortest_paths/astar/mod.rs
index 4109c44..f1c0e4a 100644
--- a/crates/algorithms/src/shortest_paths/astar/mod.rs
+++ b/crates/algorithms/src/shortest_paths/astar/mod.rs
@@ -12,8 +12,9 @@
 use error_stack::Result;
 use petgraph_core::{
     edge::marker::{Directed, Undirected},
-    id::AssociativeGraphId,
-    DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
+    node::NodeId,
+    storage::AuxiliaryGraphStorage,
+    DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage,
 };
 
 use self::r#impl::AStarImpl;
@@ -80,12 +81,12 @@
     /// let algorithm = AStar::directed().with_heuristic(heuristic);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node((0, 1)).id();
-    /// let b = *graph.insert_node((2, 2)).id();
+    /// let a = graph.insert_node((0, 1)).id();
+    /// let b = graph.insert_node((2, 2)).id();
     ///
-    /// graph.insert_edge(5, &a, &b);
+    /// graph.insert_edge(5, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b).expect("path exists");
+    /// let path = algorithm.path_between(&graph, a, b).expect("path exists");
     /// assert_eq!(path.cost().into_value(), 5);
     /// ```
     pub fn directed() -> Self {
@@ -144,13 +145,12 @@
     fn call<'graph: 'this, 'this, S>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
         intermediates: PredecessorMode,
     ) -> Result<AStarImpl<'graph, 'this, S, E, H, impl Connections<'graph, S> + 'this>, AStarError>
     where
         S: DirectedGraphStorage,
-        S::NodeId: AssociativeGraphId<S>,
         E: GraphCost<S>,
         E::Value: AStarMeasure,
         H: GraphHeuristic<S, Value = E::Value>,
@@ -171,13 +171,12 @@
     fn call<'graph: 'this, 'this, S>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
         intermediates: PredecessorMode,
     ) -> Result<AStarImpl<'graph, 'this, S, E, H, impl Connections<'graph, S> + 'this>, AStarError>
     where
         S: GraphStorage,
-        S::NodeId: AssociativeGraphId<S>,
         E: GraphCost<S>,
         E::Value: AStarMeasure,
         H: GraphHeuristic<S, Value = E::Value>,
@@ -200,7 +199,6 @@
 impl<S, E, H> ShortestPath<S> for AStar<Undirected, E, H>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: AStarMeasure,
     H: GraphHeuristic<S, Value = E::Value>,
@@ -211,7 +209,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let sources = graph.nodes().map(|node| node.id());
 
@@ -225,7 +223,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let targets = graph.nodes().map(|node| node.id());
 
@@ -239,8 +237,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         self.call(graph, source, target, PredecessorMode::Record)
             .ok()?
@@ -269,7 +267,6 @@
 impl<S, E, H> ShortestDistance<S> for AStar<Undirected, E, H>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: AStarMeasure,
     H: GraphHeuristic<S, Value = E::Value>,
@@ -280,7 +277,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let sources = graph.nodes().map(|node| node.id());
 
@@ -294,7 +291,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let targets = graph.nodes().map(|node| node.id());
 
@@ -308,8 +305,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         self.call(graph, source, target, PredecessorMode::Discard)
             .ok()?
@@ -339,7 +336,6 @@
 impl<S, E, H> ShortestPath<S> for AStar<Directed, E, H>
 where
     S: DirectedGraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: AStarMeasure,
     H: GraphHeuristic<S, Value = E::Value>,
@@ -350,7 +346,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let sources = graph.nodes().map(|node| node.id());
 
@@ -364,7 +360,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let targets = graph.nodes().map(|node| node.id());
 
@@ -378,8 +374,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         self.call(graph, source, target, PredecessorMode::Record)
             .ok()?
@@ -408,7 +404,6 @@
 impl<S, E, H> ShortestDistance<S> for AStar<Directed, E, H>
 where
     S: DirectedGraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: AStarMeasure,
     H: GraphHeuristic<S, Value = E::Value>,
@@ -419,7 +414,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let sources = graph.nodes().map(|node| node.id());
 
@@ -433,7 +428,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let targets = graph.nodes().map(|node| node.id());
 
@@ -447,8 +442,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         self.call(graph, source, target, PredecessorMode::Discard)
             .ok()?
diff --git a/crates/algorithms/src/shortest_paths/astar/tests.rs b/crates/algorithms/src/shortest_paths/astar/tests.rs
index 8ff18c1..ee676c2 100644
--- a/crates/algorithms/src/shortest_paths/astar/tests.rs
+++ b/crates/algorithms/src/shortest_paths/astar/tests.rs
@@ -4,7 +4,7 @@
 use numi::borrow::Moo;
 use ordered_float::NotNan;
 use petgraph_core::{edge::marker::Directed, Edge, GraphStorage, Node};
-use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
+use petgraph_dino::{DiDinoGraph, DinoStorage};
 use petgraph_utils::{graph, GraphCollection};
 
 use crate::shortest_paths::{AStar, ShortestDistance, ShortestPath};
@@ -21,7 +21,7 @@
         x: "X",
         y: "Y",
         z: "Z",
-    ] as NodeId,
+    ],
     [
         su: s -> u: 10,
         sx: s -> x: 5,
@@ -33,7 +33,7 @@
         xy: x -> y: 2,
         ys: y -> s: 7,
         yv: y -> v: 6,
-    ] as EdgeId
+    ]
 );
 
 #[derive(Debug, Copy, Clone, PartialEq)]
@@ -62,7 +62,7 @@
         e: Point { x: 3.0, y: 3.0 },
         f: Point { x: 4.0, y: 2.0 },
         g: Point { x: 5.0, y: 5.0 },
-    ] as NodeId,
+    ],
     [
         ab: a -> b: @{ a.distance(*b) },
         ad: a -> d: @{ a.distance(*d) },
@@ -71,7 +71,7 @@
         ce: c -> e: @{ c.distance(*e) },
         ef: e -> f: @{ e.distance(*f) },
         de: d -> e: @{ d.distance(*e) },
-    ] as EdgeId
+    ]
 );
 
 const fn no_heuristic<'a, S>(_: Node<'a, S>, _: Node<'a, S>) -> Moo<'a, usize>
@@ -255,17 +255,17 @@
         b: "B",
         c: "C",
         d: "D",
-    ] as NodeId,
+    ],
     [
         ab: a -> b: 3,
         bc: b -> c: 3,
         cd: c -> d: 3,
         ac: a -> c: 8,
         ad: a -> d: 10,
-    ] as EdgeId
+    ]
 );
 
-fn admissible_inconsistent<'a, S>(source: Node<'a, S>, target: Node<'a, S>) -> Moo<'a, usize>
+fn admissible_inconsistent<'a, S>(source: Node<'a, S>, _target: Node<'a, S>) -> Moo<'a, usize>
 where
     S: GraphStorage,
     S::NodeWeight: AsRef<str>,
@@ -311,14 +311,14 @@
         c: 'C',
         d: 'D',
         e: 'E',
-    ] as NodeId,
+    ],
     [
         ab: a -> b: 2,
         ac: a -> c: 3,
         bd: b -> d: 3,
         cd: c -> d: 1,
         de: d -> e: 1,
-    ] as EdgeId
+    ]
 );
 
 #[test]
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs b/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs
index f0b9963..b25907d 100644
--- a/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs
@@ -5,7 +5,7 @@
 use fxhash::FxBuildHasher;
 use hashbrown::HashMap;
 use numi::num::{identity::Zero, ops::AddRef};
-use petgraph_core::{Graph, GraphStorage, Node};
+use petgraph_core::{node::NodeId, Graph, GraphStorage, Node};
 
 use super::error::BellmanFordError;
 use crate::shortest_paths::{
@@ -26,7 +26,6 @@
 ) -> bool
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -52,7 +51,7 @@
 ) -> bool
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
+    NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -94,20 +93,14 @@
     }
 }
 
-struct Heuristic<S>
-where
-    S: GraphStorage,
-{
+// TODO: make use of auxiliary graph storage
+struct Heuristic {
     enabled: bool,
-    recent_update: HashMap<S::NodeId, (S::NodeId, S::NodeId)>,
-    predecessor: HashMap<S::NodeId, S::NodeId>,
+    recent_update: HashMap<NodeId, (NodeId, NodeId)>,
+    predecessor: HashMap<NodeId, NodeId>,
 }
 
-impl<S> Heuristic<S>
-where
-    S: GraphStorage,
-    S::NodeId: Eq + Hash,
-{
+impl Heuristic {
     fn new(enabled: bool) -> Self {
         Self {
             enabled,
@@ -116,11 +109,7 @@
         }
     }
 
-    fn update(
-        &mut self,
-        source: S::NodeId,
-        target: S::NodeId,
-    ) -> core::result::Result<(), S::NodeId> {
+    fn update(&mut self, source: NodeId, target: NodeId) -> core::result::Result<(), NodeId> {
         if !self.enabled {
             return Ok(());
         }
@@ -168,14 +157,13 @@
     candidate_order: CandidateOrder,
     negative_cycle_heuristics: bool,
 
-    distances: HashMap<S::NodeId, E::Value, FxBuildHasher>,
-    predecessors: HashMap<S::NodeId, Vec<Node<'graph, S>>, FxBuildHasher>,
+    distances: HashMap<NodeId, E::Value, FxBuildHasher>,
+    predecessors: HashMap<NodeId, Vec<Node<'graph, S>>, FxBuildHasher>,
 }
 
 impl<'graph: 'parent, 'parent, S, E, G> ShortestPathFasterImpl<'graph, 'parent, S, E, G>
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
     G: Connections<'graph, S>,
@@ -186,7 +174,7 @@
         edge_cost: &'parent E,
         connections: G,
 
-        source: S::NodeId,
+        source: NodeId,
 
         predecessor_mode: PredecessorMode,
         candidate_order: CandidateOrder,
@@ -226,10 +214,10 @@
     /// Inner Relaxation Loop for the Bellman-Ford algorithm, an implementation of SPFA.
     ///
     /// Based on [networkx](https://github.com/networkx/networkx/blob/f93f0e2a066fc456aa447853af9d00eec1058542/networkx/algorithms/shortest_paths/weighted.py#L1363)
-    fn relax(&mut self) -> core::result::Result<(), S::NodeId> {
+    fn relax(&mut self) -> core::result::Result<(), NodeId> {
         // we always need to record predecessors to be able to skip relaxations
         let mut queue = DoubleEndedQueue::new();
-        let mut heuristic: Heuristic<S> = Heuristic::new(self.negative_cycle_heuristics);
+        let mut heuristic = Heuristic::new(self.negative_cycle_heuristics);
         let mut occurrences = HashMap::new();
         let num_nodes = self.graph.num_nodes();
 
@@ -325,7 +313,7 @@
         Ok(())
     }
 
-    pub(crate) fn between(mut self, target: S::NodeId) -> Option<Route<'graph, S, E::Value>> {
+    pub(crate) fn between(mut self, target: NodeId) -> Option<Route<'graph, S, E::Value>> {
         let cost = self.distances.remove(&target)?;
         let target = self.graph.node(target)?;
 
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs b/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs
index d0ac726..0e48f49 100644
--- a/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs
@@ -4,7 +4,7 @@
 };
 
 use numi::{
-    cast::{CastFrom, TryCastFrom},
+    cast::TryCastFrom,
     num::{identity::Zero, ops::AddRef},
 };
 
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs b/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs
index 24875b7..7190e97 100644
--- a/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs
@@ -6,11 +6,11 @@
 mod tests;
 
 use alloc::vec::Vec;
-use core::hash::Hash;
 
 use error_stack::Result;
 use petgraph_core::{
     edge::marker::{Directed, Undirected},
+    node::NodeId,
     DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage,
 };
 
@@ -96,15 +96,15 @@
     /// let algorithm = BellmanFord::directed();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_none());
     /// ```
     pub fn directed() -> Self {
@@ -132,15 +132,15 @@
     /// let algorithm = BellmanFord::undirected();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_some());
     /// ```
     pub fn undirected() -> Self {
@@ -184,12 +184,12 @@
     /// let algorithm = BellmanFord::directed().with_edge_cost(edge_cost);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge("AB", &a, &b);
+    /// graph.insert_edge("AB", a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     /// ```
     pub fn with_edge_cost<S, F>(self, edge_cost: F) -> BellmanFord<D, F>
@@ -221,12 +221,12 @@
     /// let algorithm = BellmanFord::directed().with_candidate_order(CandidateOrder::LargeLast);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     /// ```
     #[must_use]
@@ -261,12 +261,12 @@
     /// let algorithm = BellmanFord::directed().with_negative_cycle_heuristics(false);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     /// ```
     #[must_use]
@@ -283,7 +283,6 @@
 impl<S, E> ShortestPath<S> for BellmanFord<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: PartialEq + Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -293,7 +292,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
         let iter = self.path_from(graph, target)?;
 
@@ -303,7 +302,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
         ShortestPathFasterImpl::new(
             graph,
@@ -320,8 +319,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         ShortestPathFasterImpl::new(
             graph,
@@ -352,7 +351,6 @@
 impl<S, E> ShortestDistance<S> for BellmanFord<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -362,7 +360,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
         let iter = self.distance_from(graph, target)?;
 
@@ -372,7 +370,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = ShortestPathFasterImpl::new(
             graph,
@@ -390,8 +388,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         ShortestPathFasterImpl::new(
             graph,
@@ -423,7 +421,6 @@
 impl<S, E> ShortestPath<S> for BellmanFord<Directed, E>
 where
     S: DirectedGraphStorage,
-    S::NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -433,7 +430,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         ShortestPathFasterImpl::new(
             graph,
@@ -450,8 +447,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         ShortestPathFasterImpl::new(
             graph,
@@ -482,7 +479,6 @@
 impl<S, E> ShortestDistance<S> for BellmanFord<Directed, E>
 where
     S: DirectedGraphStorage,
-    S::NodeId: Eq + Hash,
     E: GraphCost<S>,
     E::Value: BellmanFordMeasure,
 {
@@ -492,7 +488,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
         let iter = ShortestPathFasterImpl::new(
             graph,
@@ -510,8 +506,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         ShortestPathFasterImpl::new(
             graph,
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs b/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs
index 3295888..3405aee 100644
--- a/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs
@@ -2,10 +2,14 @@
 use core::array;
 
 use petgraph_core::{
-    edge::marker::{Directed, Undirected},
-    Graph, GraphStorage, ManagedGraphId,
+    edge::{
+        marker::{Directed, Undirected},
+        EdgeId,
+    },
+    node::NodeId,
+    Graph, GraphStorage,
 };
-use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
+use petgraph_dino::{DiDinoGraph, DinoStorage};
 use petgraph_utils::{graph, GraphCollection};
 
 use super::BellmanFord;
@@ -26,7 +30,7 @@
         c: "C",
         d: "D",
         e: "E",
-    ] as NodeId, [
+    ], [
         ab: a -> b: 10f32,
         ac: a -> c: 5f32,
         bd: b -> d: 1f32,
@@ -37,7 +41,7 @@
         ce: c -> e: 2f32,
         ea: e -> a: 7f32,
         ed: e -> d: 6f32,
-    ] as EdgeId
+    ]
 );
 
 #[test]
@@ -80,11 +84,9 @@
     assert!(spfa.every_path(&graph).is_ok());
 }
 
-fn cycle_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], [S::EdgeId; N])
+fn cycle_graph<const N: usize, S>() -> (Graph<S>, [NodeId; N], [EdgeId; N])
 where
     S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
 {
     let mut graph = Graph::new();
 
@@ -99,11 +101,9 @@
     (graph, nodes, edges)
 }
 
-fn complete_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], Vec<S::EdgeId>)
+fn complete_graph<const N: usize, S>() -> (Graph<S>, [NodeId; N], Vec<EdgeId>)
 where
     S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
 {
     let mut graph = Graph::new();
 
@@ -120,11 +120,9 @@
     (graph, nodes, edges)
 }
 
-fn path_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], Vec<S::EdgeId>)
+fn path_graph<const N: usize, S>() -> (Graph<S>, [NodeId; N], Vec<EdgeId>)
 where
     S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
 {
     let mut graph = Graph::new();
 
diff --git a/crates/algorithms/src/shortest_paths/common/closures.rs b/crates/algorithms/src/shortest_paths/common/closures.rs
index 90fd2ed..4b360cc 100644
--- a/crates/algorithms/src/shortest_paths/common/closures.rs
+++ b/crates/algorithms/src/shortest_paths/common/closures.rs
@@ -1,10 +1,6 @@
-use core::marker::PhantomData;
-
 use numi::borrow::Moo;
 use petgraph_core::{Edge, GraphStorage, Node};
 
-use crate::shortest_paths::GraphCost;
-
 pub fn cost<S, T>(closure: impl Fn(Edge<S>) -> Moo<T>) -> impl Fn(Edge<S>) -> Moo<T> {
     closure
 }
@@ -40,12 +36,12 @@
 
         graph.insert_edge(7, a, b);
 
-        let path = algorithm.path_between(&graph, a, b).expect("path exists");
+        let _path = algorithm.path_between(&graph, a, b).expect("path exists");
     }
 
     #[test]
     fn bind_heuristic() {
-        let closure = heuristic(|source, target| Moo::Owned(0i32));
+        let closure = heuristic(|_source, _target| Moo::Owned(0i32));
 
         let algorithm = AStar::undirected().with_heuristic(closure);
 
@@ -55,12 +51,6 @@
 
         graph.insert_edge(7i32, a, b);
 
-        let path = algorithm.path_between(&graph, a, b).expect("path exists");
+        let _path = algorithm.path_between(&graph, a, b).expect("path exists");
     }
 }
-
-// pub fn bind_heuristic<S, T>(
-//     closure: impl Fn(Node<S>, Node<S>) -> Moo<T>,
-// ) -> impl Fn(Node<S>, Node<S>) -> Moo<T> {
-//     closure
-// }
diff --git a/crates/algorithms/src/shortest_paths/common/connections.rs b/crates/algorithms/src/shortest_paths/common/connections.rs
index ad1006a..8d8d9f5 100644
--- a/crates/algorithms/src/shortest_paths/common/connections.rs
+++ b/crates/algorithms/src/shortest_paths/common/connections.rs
@@ -5,6 +5,7 @@
         marker::{Directed, Undirected},
         Direction,
     },
+    node::NodeId,
     DirectedGraphStorage, Edge, GraphDirectionality, GraphStorage,
 };
 
@@ -45,14 +46,14 @@
 where
     S: GraphStorage + 'a,
 {
-    fn connections(&self, node: S::NodeId) -> impl Iterator<Item = Edge<'a, S>> + 'a;
+    fn connections(&self, node: NodeId) -> impl Iterator<Item = Edge<'a, S>> + 'a;
 }
 
 impl<'graph, S> Connections<'graph, S> for NodeConnections<'graph, S, Directed>
 where
     S: DirectedGraphStorage,
 {
-    fn connections(&self, node: S::NodeId) -> impl Iterator<Item = Edge<'graph, S>> + 'graph {
+    fn connections(&self, node: NodeId) -> impl Iterator<Item = Edge<'graph, S>> + 'graph {
         self.storage
             .node_directed_connections(node, Direction::Outgoing)
     }
@@ -62,7 +63,7 @@
 where
     S: GraphStorage,
 {
-    fn connections(&self, node: S::NodeId) -> impl Iterator<Item = Edge<'graph, S>> + 'graph {
+    fn connections(&self, node: NodeId) -> impl Iterator<Item = Edge<'graph, S>> + 'graph {
         self.storage.node_connections(node)
     }
 }
diff --git a/crates/algorithms/src/shortest_paths/common/mod.rs b/crates/algorithms/src/shortest_paths/common/mod.rs
index ea32da1..979a59f 100644
--- a/crates/algorithms/src/shortest_paths/common/mod.rs
+++ b/crates/algorithms/src/shortest_paths/common/mod.rs
@@ -1,5 +1,5 @@
 //! Common utility types, traits and functions for shortest paths algorithms.
-mod closures;
+pub(super) mod closures;
 pub(super) mod connections;
 pub(super) mod cost;
 pub(super) mod path;
diff --git a/crates/algorithms/src/shortest_paths/common/path.rs b/crates/algorithms/src/shortest_paths/common/path.rs
index 25a4192..5874b8f 100644
--- a/crates/algorithms/src/shortest_paths/common/path.rs
+++ b/crates/algorithms/src/shortest_paths/common/path.rs
@@ -102,7 +102,6 @@
 impl<S> Display for Path<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Display,
 {
     fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
         Display::fmt(&self.source.id(), f)?;
diff --git a/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs b/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
index 97783ff..5522118 100644
--- a/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
+++ b/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
@@ -1,6 +1,5 @@
 use alloc::collections::VecDeque;
 use core::{
-    hash::Hash,
     iter::Sum,
     ops::{Add, Div},
 };
@@ -8,7 +7,7 @@
 use fxhash::FxBuildHasher;
 use hashbrown::HashSet;
 use numi::{cast::TryCastFrom, num::identity::Zero};
-use petgraph_core::{GraphStorage, Node};
+use petgraph_core::{node::NodeId, GraphStorage, Node};
 
 pub(in crate::shortest_paths) struct DoubleEndedQueueItem<'graph, S, T>
 where
@@ -42,13 +41,12 @@
     S: GraphStorage,
 {
     queue: VecDeque<DoubleEndedQueueItem<'graph, S, T>>,
-    active: HashSet<S::NodeId, FxBuildHasher>,
+    active: HashSet<NodeId, FxBuildHasher>,
 }
 
 impl<'graph, S, T> DoubleEndedQueue<'graph, S, T>
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
 {
     pub(in crate::shortest_paths) fn new() -> Self {
         Self {
@@ -143,7 +141,7 @@
         self.queue.len()
     }
 
-    pub(in crate::shortest_paths) fn contains_node(&self, node: &S::NodeId) -> bool {
+    pub(in crate::shortest_paths) fn contains_node(&self, node: &NodeId) -> bool {
         self.active.contains(node)
     }
 }
diff --git a/crates/algorithms/src/shortest_paths/common/queue/priority.rs b/crates/algorithms/src/shortest_paths/common/queue/priority.rs
index 24de078..118352e 100644
--- a/crates/algorithms/src/shortest_paths/common/queue/priority.rs
+++ b/crates/algorithms/src/shortest_paths/common/queue/priority.rs
@@ -2,17 +2,20 @@
 use core::cmp::Ordering;
 
 use petgraph_core::{
-    id::{AssociativeGraphId, BooleanMapper},
-    GraphStorage,
+    node::NodeId,
+    storage::{
+        auxiliary::{BooleanGraphStorage, FrequencyHint, Hints, OccupancyHint, PerformanceHint},
+        AuxiliaryGraphStorage,
+    },
 };
 
-pub(in crate::shortest_paths) struct PriorityQueueItem<I, T> {
-    pub(in crate::shortest_paths) node: I,
+pub(in crate::shortest_paths) struct PriorityQueueItem<T> {
+    pub(in crate::shortest_paths) node: NodeId,
 
     pub(in crate::shortest_paths) priority: T,
 }
 
-impl<I, T> PartialEq for PriorityQueueItem<I, T>
+impl<T> PartialEq for PriorityQueueItem<T>
 where
     T: PartialEq,
 {
@@ -21,9 +24,9 @@
     }
 }
 
-impl<I, T> Eq for PriorityQueueItem<I, T> where T: Eq {}
+impl<T> Eq for PriorityQueueItem<T> where T: Eq {}
 
-impl<I, T> PartialOrd for PriorityQueueItem<I, T>
+impl<T> PartialOrd for PriorityQueueItem<T>
 where
     T: PartialOrd,
 {
@@ -32,7 +35,7 @@
     }
 }
 
-impl<I, T> Ord for PriorityQueueItem<I, T>
+impl<T> Ord for PriorityQueueItem<T>
 where
     T: Ord,
 {
@@ -41,48 +44,52 @@
     }
 }
 
-pub(in crate::shortest_paths) struct PriorityQueue<'a, S, T>
+pub(in crate::shortest_paths) struct PriorityQueue<'graph, S, T>
 where
-    S: GraphStorage + 'a,
-    S::NodeId: AssociativeGraphId<S>,
+    S: AuxiliaryGraphStorage + 'graph,
     T: Ord,
 {
-    heap: BinaryHeap<PriorityQueueItem<S::NodeId, T>>,
+    heap: BinaryHeap<PriorityQueueItem<T>>,
     pub(crate) check_admissibility: bool,
 
-    flags: <S::NodeId as AssociativeGraphId<S>>::BooleanMapper<'a>,
+    flags: S::BooleanNodeStorage<'graph>,
 }
 
-impl<'a, S, T> PriorityQueue<'a, S, T>
+impl<'graph, S, T> PriorityQueue<'graph, S, T>
 where
-    S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
+    S: AuxiliaryGraphStorage + 'graph,
     T: Ord,
 {
     #[inline]
-    pub(in crate::shortest_paths) fn new(storage: &'a S) -> Self {
+    pub(in crate::shortest_paths) fn new(storage: &'graph S) -> Self {
         Self {
             heap: BinaryHeap::new(),
             check_admissibility: true,
-            flags: <S::NodeId as AssociativeGraphId<S>>::boolean_mapper(storage),
+            flags: storage.boolean_node_storage(Hints {
+                performance: PerformanceHint {
+                    read: FrequencyHint::Frequent,
+                    write: FrequencyHint::Infrequent,
+                },
+                occupancy: OccupancyHint::Dense,
+            }),
         }
     }
 
-    pub(in crate::shortest_paths) fn push(&mut self, node: S::NodeId, priority: T) {
+    pub(in crate::shortest_paths) fn push(&mut self, node: NodeId, priority: T) {
         self.heap.push(PriorityQueueItem { node, priority });
     }
 
-    pub(in crate::shortest_paths) fn visit(&mut self, id: S::NodeId) {
+    pub(in crate::shortest_paths) fn visit(&mut self, id: NodeId) {
         self.flags.set(id, true);
     }
 
     #[inline]
-    pub(in crate::shortest_paths) fn has_been_visited(&self, id: S::NodeId) -> bool {
-        self.flags.index(id)
+    pub(in crate::shortest_paths) fn has_been_visited(&self, id: NodeId) -> bool {
+        self.flags.get(id).unwrap_or(false)
     }
 
     #[inline]
-    pub(in crate::shortest_paths) fn decrease_priority(&mut self, node: S::NodeId, priority: T) {
+    pub(in crate::shortest_paths) fn decrease_priority(&mut self, node: NodeId, priority: T) {
         if self.check_admissibility && self.has_been_visited(node) {
             return;
         }
@@ -91,7 +98,7 @@
     }
 
     #[inline]
-    pub(in crate::shortest_paths) fn pop_min(&mut self) -> Option<PriorityQueueItem<S::NodeId, T>> {
+    pub(in crate::shortest_paths) fn pop_min(&mut self) -> Option<PriorityQueueItem<T>> {
         loop {
             let item = self.heap.pop()?;
 
diff --git a/crates/algorithms/src/shortest_paths/common/route.rs b/crates/algorithms/src/shortest_paths/common/route.rs
index 23829fc..dfd6abb 100644
--- a/crates/algorithms/src/shortest_paths/common/route.rs
+++ b/crates/algorithms/src/shortest_paths/common/route.rs
@@ -74,7 +74,6 @@
 impl<S, T> Display for Route<'_, S, T>
 where
     S: GraphStorage,
-    S::NodeId: Display,
     T: Display,
 {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
@@ -229,7 +228,6 @@
 impl<S, T> Display for DirectRoute<'_, S, T>
 where
     S: GraphStorage,
-    S::NodeId: Display,
     T: Display,
 {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
diff --git a/crates/algorithms/src/shortest_paths/common/tests.rs b/crates/algorithms/src/shortest_paths/common/tests.rs
index 629ff62..02bf986 100644
--- a/crates/algorithms/src/shortest_paths/common/tests.rs
+++ b/crates/algorithms/src/shortest_paths/common/tests.rs
@@ -1,8 +1,5 @@
 use alloc::vec::Vec;
-use core::{
-    fmt::{Debug, Display},
-    hash::Hash,
-};
+use core::fmt::Debug;
 
 use error_stack::Result;
 use hashbrown::HashMap;
@@ -39,12 +36,13 @@
 }
 
 pub(in crate::shortest_paths) use expected;
+use petgraph_core::node::NodeId;
 
-pub(in crate::shortest_paths) struct Expect<N, T> {
-    pub(in crate::shortest_paths) source: N,
-    pub(in crate::shortest_paths) target: N,
+pub(in crate::shortest_paths) struct Expect<T> {
+    pub(in crate::shortest_paths) source: NodeId,
+    pub(in crate::shortest_paths) target: NodeId,
 
-    pub(in crate::shortest_paths) transit: Vec<N>,
+    pub(in crate::shortest_paths) transit: Vec<NodeId>,
 
     pub(in crate::shortest_paths) cost: T,
 }
@@ -55,7 +53,7 @@
 {
     graph: &'a Graph<S>,
     algorithm: &'a A,
-    expected: &'a [Expect<S::NodeId, T>],
+    expected: &'a [Expect<T>],
 }
 
 impl<'a, S, A, T> TestCase<'a, S, A, T>
@@ -65,7 +63,7 @@
     pub(crate) const fn new(
         graph: &'a Graph<S>,
         algorithm: &'a A,
-        expected: &'a [Expect<<S as GraphStorage>::NodeId, T>],
+        expected: &'a [Expect<T>],
     ) -> Self {
         Self {
             graph,
@@ -78,7 +76,6 @@
 impl<'a, S, A, T> TestCase<'a, S, A, T>
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash + Debug + Display,
     A: ShortestPath<S, Cost = T>,
     T: PartialEq + Debug,
 {
@@ -120,7 +117,7 @@
     }
 
     #[track_caller]
-    pub(in crate::shortest_paths) fn assert_path_from(&self, source: S::NodeId) {
+    pub(in crate::shortest_paths) fn assert_path_from(&self, source: NodeId) {
         self.assert_path_routes(self.algorithm.path_from(self.graph, source));
     }
 }
@@ -128,7 +125,6 @@
 impl<'a, S, A, T> TestCase<'a, S, A, T>
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash + Debug + Display,
     A: ShortestDistance<S, Cost = T>,
     T: PartialEq + Debug,
 {
@@ -174,7 +170,7 @@
     }
 
     #[track_caller]
-    pub(in crate::shortest_paths) fn assert_distance_from(&self, source: S::NodeId) {
+    pub(in crate::shortest_paths) fn assert_distance_from(&self, source: NodeId) {
         self.assert_distance_routes(self.algorithm.distance_from(self.graph, source));
     }
 }
diff --git a/crates/algorithms/src/shortest_paths/common/transit.rs b/crates/algorithms/src/shortest_paths/common/transit.rs
index cd7f47e..ebf7147 100644
--- a/crates/algorithms/src/shortest_paths/common/transit.rs
+++ b/crates/algorithms/src/shortest_paths/common/transit.rs
@@ -1,12 +1,10 @@
 use alloc::{vec, vec::Vec};
-use core::{
-    hash::{BuildHasher, Hash},
-    iter,
-};
+use core::{hash::BuildHasher, iter};
 
 use hashbrown::{HashMap, HashSet};
 use petgraph_core::{
-    id::{AssociativeGraphId, AttributeMapper},
+    node::NodeId,
+    storage::{auxiliary::SecondaryGraphStorage, AuxiliaryGraphStorage},
     GraphStorage, Node,
 };
 
@@ -17,26 +15,25 @@
 }
 
 pub(in crate::shortest_paths) fn reconstruct_path_to<S>(
-    predecessors: &<S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'_, Option<S::NodeId>>,
-    target: S::NodeId,
-) -> Vec<S::NodeId>
+    predecessors: &S::SecondaryNodeStorage<'_, Option<NodeId>>,
+    target: NodeId,
+) -> Vec<NodeId>
 where
-    S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
+    S: AuxiliaryGraphStorage,
 {
     let mut current = target;
 
     let mut path = Vec::new();
 
     loop {
-        let &Some(node) = predecessors.index(current) else {
+        let Some(node) = predecessors.get(current).copied().flatten() else {
             // this case should in theory _never_ happen, as the next statement
             // terminates if the next node is `None` (we're at a source node)
             // we do it this way, so that we don't need to push and then pop immediately.
             break;
         };
 
-        if predecessors.index(node).is_none() {
+        if predecessors.get(node).copied().flatten().is_none() {
             // we have reached the source node
             break;
         }
@@ -54,13 +51,12 @@
 ///
 /// This has been adapted from the [NetworkX implementation](https://github.com/networkx/networkx/blob/f93f0e2a066fc456aa447853af9d00eec1058542/networkx/algorithms/shortest_paths/generic.py#L655)
 pub(in crate::shortest_paths) fn reconstruct_paths_between<'a, 'graph, S, H>(
-    predecessors: &'a HashMap<S::NodeId, Vec<Node<'graph, S>>, H>,
-    source: S::NodeId,
+    predecessors: &'a HashMap<NodeId, Vec<Node<'graph, S>>, H>,
+    source: NodeId,
     target: Node<'graph, S>,
 ) -> impl Iterator<Item = Vec<Node<'graph, S>>> + 'a
 where
     S: GraphStorage,
-    S::NodeId: Eq + Hash,
     H: BuildHasher,
 {
     let mut seen = HashSet::new();
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/iter.rs b/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
index f2972f3..84cf81e 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
@@ -4,7 +4,11 @@
 use error_stack::{Report, Result};
 use numi::num::{identity::Zero, ops::AddRef};
 use petgraph_core::{
-    id::{AssociativeGraphId, AttributeMapper},
+    node::NodeId,
+    storage::{
+        auxiliary::{FrequencyHint, Hints, OccupancyHint, PerformanceHint, SecondaryGraphStorage},
+        AuxiliaryGraphStorage,
+    },
     Graph, GraphStorage, Node,
 };
 
@@ -23,7 +27,6 @@
 pub(super) struct DijkstraIter<'graph: 'parent, 'parent, S, E, G>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
 {
@@ -38,18 +41,17 @@
     num_nodes: usize,
 
     init: bool,
-    next: Option<PriorityQueueItem<S::NodeId, E::Value>>,
+    next: Option<PriorityQueueItem<E::Value>>,
 
     predecessor_mode: PredecessorMode,
 
-    distances: <S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'graph, E::Value>,
-    predecessors: <S::NodeId as AssociativeGraphId<S>>::AttributeMapper<'graph, Option<S::NodeId>>,
+    distances: S::SecondaryNodeStorage<'graph, E::Value>,
+    predecessors: S::SecondaryNodeStorage<'graph, Option<NodeId>>,
 }
 
 impl<'graph: 'parent, 'parent, S, E, G> DijkstraIter<'graph, 'parent, S, E, G>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
     G: Connections<'graph, S>,
@@ -60,7 +62,7 @@
         edge_cost: &'parent E,
         connections: G,
 
-        source: S::NodeId,
+        source: NodeId,
 
         predecessor_mode: PredecessorMode,
     ) -> Result<Self, DijkstraError> {
@@ -70,11 +72,22 @@
 
         let queue = PriorityQueue::new(graph.storage());
 
-        let mut distances = <S::NodeId as AssociativeGraphId<S>>::attribute_mapper(graph.storage());
+        let mut distances = graph.storage().secondary_node_storage(Hints {
+            performance: PerformanceHint {
+                read: FrequencyHint::Frequent,
+                write: FrequencyHint::Frequent,
+            },
+            occupancy: OccupancyHint::Dense,
+        });
         distances.set(source, E::Value::zero());
 
-        let mut predecessors =
-            <S::NodeId as AssociativeGraphId<S>>::attribute_mapper(graph.storage());
+        let mut predecessors = graph.storage().secondary_node_storage(Hints {
+            performance: PerformanceHint {
+                read: FrequencyHint::Infrequent,
+                write: FrequencyHint::Frequent,
+            },
+            occupancy: OccupancyHint::Dense,
+        });
         if predecessor_mode == PredecessorMode::Record {
             predecessors.set(source, None);
         }
@@ -98,7 +111,6 @@
 impl<'graph: 'parent, 'parent, S, E, G> Iterator for DijkstraIter<'graph, 'parent, S, E, G>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
     G: Connections<'graph, S>,
@@ -158,9 +170,7 @@
                 self.predecessors.set(target, Some(node));
             }
 
-            // if let Some(target) = self.graph.node(target) {
             self.queue.decrease_priority(target, alternative);
-            // }
         }
 
         // this is what makes this special: instead of getting the next node as the start of next
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/mod.rs b/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
index 9dec7ba..adc3e0c 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
@@ -11,8 +11,9 @@
 use error_stack::Result;
 use petgraph_core::{
     edge::marker::{Directed, Undirected},
-    id::AssociativeGraphId,
-    DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
+    node::NodeId,
+    storage::AuxiliaryGraphStorage,
+    DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage,
 };
 
 use self::iter::DijkstraIter;
@@ -65,15 +66,15 @@
     /// let algorithm = Dijkstra::directed();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_none());
     /// ```
     #[must_use]
@@ -100,15 +101,15 @@
     /// let algorithm = Dijkstra::undirected();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(2, &a, &b);
+    /// graph.insert_edge(2, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_some());
     /// ```
     #[must_use]
@@ -151,12 +152,12 @@
     /// let algorithm = Dijkstra::directed().with_edge_cost(edge_cost);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge("AB", &a, &b);
+    /// graph.insert_edge("AB", a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     /// ```
     pub fn with_edge_cost<S, F>(self, edge_cost: F) -> Dijkstra<D, F>
@@ -174,7 +175,6 @@
 impl<S, E> ShortestPath<S> for Dijkstra<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
 {
@@ -184,7 +184,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
         let iter = self.path_from(graph, target)?;
 
@@ -194,7 +194,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         DijkstraIter::new(
             graph,
@@ -221,7 +221,6 @@
 impl<S, E> ShortestPath<S> for Dijkstra<Directed, E>
 where
     S: DirectedGraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
 {
@@ -231,7 +230,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         DijkstraIter::new(
             graph,
@@ -258,7 +257,6 @@
 impl<S, E> ShortestDistance<S> for Dijkstra<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
 {
@@ -268,7 +266,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
         let iter = self.distance_from(graph, target)?;
 
@@ -278,7 +276,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = DijkstraIter::new(
             graph,
@@ -307,7 +305,6 @@
 impl<S, E> ShortestDistance<S> for Dijkstra<Directed, E>
 where
     S: DirectedGraphStorage,
-    S::NodeId: AssociativeGraphId<S>,
     E: GraphCost<S>,
     E::Value: DijkstraMeasure,
 {
@@ -317,7 +314,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
         let iter = DijkstraIter::new(
             graph,
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/tests.rs b/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
index c6ee7fa..905f7ea 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
@@ -2,7 +2,7 @@
 
 use numi::borrow::Moo;
 use petgraph_core::{Edge, GraphStorage};
-use petgraph_dino::{DiDinoGraph, EdgeId, NodeId};
+use petgraph_dino::DiDinoGraph;
 use petgraph_utils::{graph, GraphCollection};
 
 use crate::shortest_paths::{
@@ -22,7 +22,7 @@
         c: "C",
         d: "D",
         e: "E",
-    ] as NodeId, [
+    ], [
         ab: a -> b: 10,
         ac: a -> c: 5,
         bd: b -> d: 1,
@@ -33,12 +33,10 @@
         ce: c -> e: 2,
         ea: e -> a: 7,
         ed: e -> d: 6,
-    ] as EdgeId
+    ]
 );
 
-fn networkx_directed_expect_from(
-    nodes: &networkx::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, i32>> {
+fn networkx_directed_expect_from(nodes: &networkx::NodeCollection) -> Vec<Expect<i32>> {
     expected!(nodes; [
         a -()> a: 0,
         a -(c)> b: 8,
@@ -58,7 +56,7 @@
         d: "D",
         e: "E",
         f: "F",
-    ] as NodeId, [
+    ], [
         ab: a -> b: "apple",
         bc: b -> c: "cat",
         cd: c -> d: "giraffe",
@@ -66,7 +64,7 @@
         ef: e -> f: "banana",
         fa: f -> a: "bear",
         ad: a -> d: "elephant",
-    ] as EdgeId
+    ]
 );
 
 // TODO: multigraph
@@ -91,9 +89,7 @@
     TestCase::new(&graph, &dijkstra, &expected).assert_distance_from(nodes.a);
 }
 
-fn random_directed_expect_from(
-    nodes: &random::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, usize>> {
+fn random_directed_expect_from(nodes: &random::NodeCollection) -> Vec<Expect<usize>> {
     expected!(nodes; [
         a -()> a: 0,
         a -()> b: 5,
@@ -132,9 +128,7 @@
     TestCase::new(&graph, &dijkstra, &expected).assert_distance_from(nodes.a);
 }
 
-fn networkx_undirected_expect_from(
-    nodes: &networkx::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, i32>> {
+fn networkx_undirected_expect_from(nodes: &networkx::NodeCollection) -> Vec<Expect<i32>> {
     expected!(nodes; [
         a -()> a: 0,
         a -(c)> b: 7,
@@ -164,9 +158,7 @@
     TestCase::new(&graph, &dijkstra, &expected).assert_distance_from(nodes.a);
 }
 
-fn random_undirected_expect_from(
-    nodes: &random::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, usize>> {
+fn random_undirected_expect_from(nodes: &random::NodeCollection) -> Vec<Expect<usize>> {
     expected!(nodes; [
         a -()> a: 0,
         a -()> b: 5,
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/error.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/error.rs
index 534fdce..5b835bb 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/error.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/error.rs
@@ -1,12 +1,26 @@
+use alloc::vec::Vec;
 use core::{
     fmt,
     fmt::{Debug, Display, Formatter},
 };
 
 use error_stack::Context;
+use petgraph_core::node::NodeId;
 
+/// An error that can occur during the Floyd-Warshall algorithm.
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
 pub enum FloydWarshallError {
+    /// The graph contains a negative cycle.
+    ///
+    /// The error attaches all nodes where a negative cycle was detected via the [`NegativeCycle`]
+    /// type.
+    ///
+    /// Note that multiple negative cycles may exist in the graph, and each attachment only means
+    /// that it is part of a negative cycle and not which cycle(s) it is part of.
+    ///
+    /// To find all negative cycles, use [`BellmanFord`] instead.
+    ///
+    /// [`BellmanFord`]: crate::shortest_paths::BellmanFord
     NegativeCycle,
 }
 
@@ -19,3 +33,18 @@
 }
 
 impl Context for FloydWarshallError {}
+
+/// A node that is part of a negative cycle.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct NegativeCycle(NodeId);
+
+impl NegativeCycle {
+    pub(crate) fn new(node: NodeId) -> Self {
+        Self(node)
+    }
+
+    /// Returns the node that is part of a negative cycle.
+    pub fn node(&self) -> NodeId {
+        self.0
+    }
+}
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
index 72082d3..95a7e1a 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
@@ -5,7 +5,7 @@
     borrow::Moo,
     num::{checked::CheckedAdd, identity::Zero},
 };
-use petgraph_core::{id::LinearGraphId, Graph, GraphStorage, Node};
+use petgraph_core::{node::NodeId, storage::SequentialGraphStorage, Graph, GraphStorage, Node};
 
 use crate::shortest_paths::{
     common::{
@@ -13,18 +13,19 @@
         route::Route,
         transit::PredecessorMode,
     },
-    floyd_warshall::{error::FloydWarshallError, matrix::SlotMatrix, FloydWarshallMeasure},
+    floyd_warshall::{
+        error::FloydWarshallError, matrix::SlotMatrix, FloydWarshallMeasure, NegativeCycle,
+    },
     Path,
 };
 
 pub(super) fn init_directed_edge_distance<'graph: 'this, 'this, S, E>(
     matrix: &mut SlotMatrix<'graph, S, Moo<'this, E::Value>>,
-    u: S::NodeId,
-    v: S::NodeId,
+    u: NodeId,
+    v: NodeId,
     value: Option<Moo<'this, E::Value>>,
 ) where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
     E: GraphCost<S>,
     E::Value: Clone,
 {
@@ -33,12 +34,11 @@
 
 pub(super) fn init_undirected_edge_distance<'graph: 'this, 'this, S, E>(
     matrix: &mut SlotMatrix<'graph, S, Moo<'this, E::Value>>,
-    u: S::NodeId,
-    v: S::NodeId,
+    u: NodeId,
+    v: NodeId,
     value: Option<Moo<'this, E::Value>>,
 ) where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
     E: GraphCost<S>,
     E::Value: Clone,
 {
@@ -50,36 +50,33 @@
 }
 
 pub(super) fn init_directed_edge_predecessor<S>(
-    matrix: &mut SlotMatrix<S, S::NodeId>,
-    u: S::NodeId,
-    v: S::NodeId,
+    matrix: &mut SlotMatrix<S, NodeId>,
+    u: NodeId,
+    v: NodeId,
 ) where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
 {
     matrix.set(u, v, Some(u));
 }
 
 pub(super) fn init_undirected_edge_predecessor<S>(
-    matrix: &mut SlotMatrix<S, S::NodeId>,
-    u: S::NodeId,
-    v: S::NodeId,
+    matrix: &mut SlotMatrix<S, NodeId>,
+    u: NodeId,
+    v: NodeId,
 ) where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
 {
     matrix.set(u, v, Some(u));
     matrix.set(v, u, Some(v));
 }
 
 fn reconstruct_path<S>(
-    matrix: &SlotMatrix<'_, S, S::NodeId>,
-    source: S::NodeId,
-    target: S::NodeId,
-) -> Vec<S::NodeId>
+    matrix: &SlotMatrix<'_, S, NodeId>,
+    source: NodeId,
+    target: NodeId,
+) -> Vec<NodeId>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
 {
     let mut path = Vec::new();
 
@@ -113,21 +110,16 @@
 }
 type InitEdgeDistanceFn<'graph, 'this, S, E> = fn(
     &mut SlotMatrix<'graph, S, Moo<'this, <E as GraphCost<S>>::Value>>,
-    <S as GraphStorage>::NodeId,
-    <S as GraphStorage>::NodeId,
+    NodeId,
+    NodeId,
     Option<Moo<'this, <E as GraphCost<S>>::Value>>,
 );
 
-type InitEdgePredecessorFn<'graph, S> = fn(
-    &mut SlotMatrix<'graph, S, <S as GraphStorage>::NodeId>,
-    <S as GraphStorage>::NodeId,
-    <S as GraphStorage>::NodeId,
-);
+type InitEdgePredecessorFn<'graph, S> = fn(&mut SlotMatrix<'graph, S, NodeId>, NodeId, NodeId);
 
 pub(super) struct FloydWarshallImpl<'graph: 'parent, 'parent, S, E>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S>,
     E: GraphCost<S>,
 {
     graph: &'graph Graph<S>,
@@ -139,14 +131,12 @@
     init_edge_predecessor: InitEdgePredecessorFn<'graph, S>,
 
     distances: SlotMatrix<'graph, S, Moo<'parent, E::Value>>,
-    predecessors: SlotMatrix<'graph, S, S::NodeId>,
+    predecessors: SlotMatrix<'graph, S, NodeId>,
 }
 
-// TODO: relax `NodeId` requirements or make `Send + Sync + 'static` across the board
 impl<'graph: 'parent, 'parent, S, E> FloydWarshallImpl<'graph, 'parent, S, E>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
     E: GraphCost<S>,
     E::Value: FloydWarshallMeasure,
 {
@@ -268,9 +258,11 @@
                 continue;
             };
 
+            let cycle = NegativeCycle::new(node);
+
             result = match result {
-                Ok(()) => Err(Report::new(FloydWarshallError::NegativeCycle).attach(node)),
-                Err(report) => Err(report.attach(node)),
+                Ok(()) => Err(Report::new(FloydWarshallError::NegativeCycle).attach(cycle)),
+                Err(report) => Err(report.attach(cycle)),
             };
         }
 
@@ -316,11 +308,7 @@
             })
     }
 
-    pub(super) fn pick(
-        self,
-        source: S::NodeId,
-        target: S::NodeId,
-    ) -> Option<Route<'graph, S, E::Value>> {
+    pub(super) fn pick(self, source: NodeId, target: NodeId) -> Option<Route<'graph, S, E::Value>> {
         let Self {
             graph,
             distances,
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
index 5915c2e..8a424fd 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
@@ -1,8 +1,8 @@
 use alloc::vec::Vec;
 
-use numi::borrow::Moo;
 use petgraph_core::{
-    id::{IndexMapper, LinearGraphId},
+    node::NodeId,
+    storage::{sequential::GraphIdBijection, SequentialGraphStorage},
     Graph, GraphStorage,
 };
 
@@ -11,9 +11,9 @@
     Discard,
 }
 
-impl<I, T> IndexMapper<T> for MatrixIndexMapper<I>
+impl<I, T> GraphIdBijection<T> for MatrixIndexMapper<I>
 where
-    I: IndexMapper<T>,
+    I: GraphIdBijection<T>,
     T: PartialEq,
 {
     fn max(&self) -> usize {
@@ -41,9 +41,8 @@
 pub(super) struct SlotMatrix<'graph, S, T>
 where
     S: GraphStorage + 'graph,
-    S::NodeId: LinearGraphId<S>,
 {
-    mapper: MatrixIndexMapper<<S::NodeId as LinearGraphId<S>>::Mapper<'graph>>,
+    mapper: MatrixIndexMapper<S::NodeIdBijection<'graph>>,
     matrix: Vec<Option<T>>,
     length: usize,
 }
@@ -51,13 +50,10 @@
 impl<'graph, S, T> SlotMatrix<'graph, S, T>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S>,
 {
     pub(crate) fn new(graph: &'graph Graph<S>) -> Self {
         let length = graph.num_nodes();
-        let mapper = MatrixIndexMapper::Store(<S::NodeId as LinearGraphId<S>>::index_mapper(
-            graph.storage(),
-        ));
+        let mapper = MatrixIndexMapper::Store(graph.storage().node_id_bijection());
 
         let mut matrix = Vec::with_capacity(length * length);
         matrix.resize_with(length * length, Default::default);
@@ -81,7 +77,7 @@
         }
     }
 
-    pub(crate) fn set(&mut self, source: S::NodeId, target: S::NodeId, value: Option<T>) {
+    pub(crate) fn set(&mut self, source: NodeId, target: NodeId, value: Option<T>) {
         if matches!(self.mapper, MatrixIndexMapper::Discard) {
             // this should never happen, even if it does, we don't want to panic here (map call)
             // so we simply return.
@@ -104,16 +100,16 @@
     /// Returns `None` if the node cannot be looked up, this only happens if you try to query for a
     /// value on an index that has not yet been set via `set`.
     ///
-    /// See the contract described on the [`IndexMapper`] for more information about the
+    /// See the contract described on the [`GraphIdBijection`] for more information about the
     /// `map/lookup` contract.
-    pub(crate) fn get(&self, source: S::NodeId, target: S::NodeId) -> Option<&T> {
+    pub(crate) fn get(&self, source: NodeId, target: NodeId) -> Option<&T> {
         let source = self.mapper.get(source)?;
         let target = self.mapper.get(target)?;
 
         self.matrix[source * self.length + target].as_ref()
     }
 
-    pub(crate) fn resolve(&self, index: usize) -> Option<S::NodeId> {
+    pub(crate) fn resolve(&self, index: usize) -> Option<NodeId> {
         self.mapper.reverse(index)
     }
 }
@@ -125,7 +121,6 @@
 impl<'a, S, T> SlotMatrix<'a, S, T>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone,
 {
     pub(crate) fn diagonal(&self) -> impl Iterator<Item = Option<&T>> + Captures<'a> + '_ {
         let len = self.length;
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
index 2049e2b..5ece800 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
@@ -11,15 +11,19 @@
 use error_stack::Result;
 use petgraph_core::{
     edge::marker::{Directed, Undirected},
-    id::LinearGraphId,
-    Graph, GraphStorage,
+    node::NodeId,
+    storage::SequentialGraphStorage,
+    DirectedGraphStorage, Graph, GraphStorage,
 };
 
 use self::r#impl::{
     init_directed_edge_distance, init_directed_edge_predecessor, init_undirected_edge_distance,
     init_undirected_edge_predecessor, FloydWarshallImpl,
 };
-pub use self::{error::FloydWarshallError, measure::FloydWarshallMeasure};
+pub use self::{
+    error::{FloydWarshallError, NegativeCycle},
+    measure::FloydWarshallMeasure,
+};
 use super::{
     common::{
         cost::{DefaultCost, GraphCost},
@@ -70,15 +74,15 @@
     /// let algorithm = FloydWarshall::directed();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(7, &a, &b);
+    /// graph.insert_edge(7, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_none());
     /// ```
     #[must_use]
@@ -104,15 +108,15 @@
     /// let algorithm = FloydWarshall::undirected();
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge(7, &a, &b);
+    /// graph.insert_edge(7, a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_some());
     /// ```
     #[must_use]
@@ -150,15 +154,15 @@
     /// let algorithm = FloydWarshall::directed().with_edge_cost(edge_cost);
     ///
     /// let mut graph = DiDinoGraph::new();
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
+    /// let a = graph.insert_node("A").id();
+    /// let b = graph.insert_node("B").id();
     ///
-    /// graph.insert_edge("AB", &a, &b);
+    /// graph.insert_edge("AB", a, b);
     ///
-    /// let path = algorithm.path_between(&graph, &a, &b);
+    /// let path = algorithm.path_between(&graph, a, b);
     /// assert!(path.is_some());
     ///
-    /// let path = algorithm.path_between(&graph, &b, &a);
+    /// let path = algorithm.path_between(&graph, b, a);
     /// assert!(path.is_none());
     /// ```
     pub fn with_edge_cost<S, F>(self, edge_cost: F) -> FloydWarshall<D, F>
@@ -176,7 +180,6 @@
 impl<S, E> ShortestPath<S> for FloydWarshall<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
     E: GraphCost<S>,
     E::Value: FloydWarshallMeasure,
 {
@@ -186,7 +189,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         FloydWarshallImpl::new(
             graph,
@@ -201,7 +204,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         FloydWarshallImpl::new(
             graph,
@@ -216,8 +219,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         let r#impl = FloydWarshallImpl::new(
             graph,
@@ -249,7 +252,6 @@
 impl<S, E> ShortestDistance<S> for FloydWarshall<Undirected, E>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
     E: GraphCost<S>,
     E::Value: FloydWarshallMeasure,
 {
@@ -259,7 +261,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = FloydWarshallImpl::new(
             graph,
@@ -277,7 +279,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = FloydWarshallImpl::new(
             graph,
@@ -295,8 +297,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         let iter = FloydWarshallImpl::new(
             graph,
@@ -328,8 +330,7 @@
 
 impl<S, E> ShortestPath<S> for FloydWarshall<Directed, E>
 where
-    S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
+    S: DirectedGraphStorage,
     E: GraphCost<S>,
     E::Value: FloydWarshallMeasure,
 {
@@ -339,7 +340,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         FloydWarshallImpl::new(
             graph,
@@ -354,7 +355,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         FloydWarshallImpl::new(
             graph,
@@ -370,8 +371,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         let r#impl = FloydWarshallImpl::new(
             graph,
@@ -402,8 +403,7 @@
 
 impl<S, E> ShortestDistance<S> for FloydWarshall<Directed, E>
 where
-    S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
+    S: DirectedGraphStorage,
     E: GraphCost<S>,
     E::Value: FloydWarshallMeasure,
 {
@@ -413,7 +413,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = FloydWarshallImpl::new(
             graph,
@@ -431,7 +431,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = FloydWarshallImpl::new(
             graph,
@@ -449,8 +449,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         let iter = FloydWarshallImpl::new(
             graph,
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
index 1bd28b0..b91d4fb 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
@@ -1,12 +1,13 @@
 use alloc::{vec, vec::Vec};
 
 use hashbrown::HashSet;
-use petgraph_dino::{DiDinoGraph, EdgeId, NodeId};
+use petgraph_core::node::NodeId;
+use petgraph_dino::DiDinoGraph;
 use petgraph_utils::{graph, GraphCollection};
 
 use crate::shortest_paths::{
     common::tests::{expected, Expect, TestCase},
-    floyd_warshall::{error::FloydWarshallError, FloydWarshall},
+    floyd_warshall::{error::FloydWarshallError, FloydWarshall, NegativeCycle},
     ShortestPath,
 };
 
@@ -28,7 +29,7 @@
         f: "F",
         g: "G",
         h: "H",
-    ] as NodeId,
+    ],
     [
         ab: a -> b: 1,
         bc: b -> c: 1,
@@ -39,10 +40,10 @@
         fg: f -> g: 1,
         gh: g -> h: 1,
         he: h -> e: 1,
-    ] as EdgeId
+    ]
 );
 
-fn uniform_expect(nodes: &uniform::NodeCollection<NodeId>) -> Vec<Expect<NodeId, usize>> {
+fn uniform_expect(nodes: &uniform::NodeCollection) -> Vec<Expect<usize>> {
     expected!(nodes; [
         a -()> a: 0,
         a -()> b: 1,
@@ -136,7 +137,7 @@
         b: "B",
         c: "C",
         d: "D",
-    ] as NodeId,
+    ],
     [
         ab: a -> b: 1,
         ac: a -> c: 4,
@@ -144,12 +145,10 @@
         bc: b -> c: 2,
         bd: b -> d: 2,
         cd: c -> d: 2,
-    ] as EdgeId
+    ]
 );
 
-fn directed_weighted_expect(
-    nodes: &weighted::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, usize>> {
+fn directed_weighted_expect(nodes: &weighted::NodeCollection) -> Vec<Expect<usize>> {
     expected!(nodes; [
         a -()> a: 0,
         a -()> b: 1,
@@ -242,9 +241,7 @@
     TestCase::new(&graph, &floyd_warshall, &expected).assert_every_distance();
 }
 
-fn undirected_weighted_expect(
-    nodes: &weighted::NodeCollection<NodeId>,
-) -> Vec<Expect<NodeId, usize>> {
+fn undirected_weighted_expect(nodes: &weighted::NodeCollection) -> Vec<Expect<usize>> {
     expected!(nodes; [
         a -()> a: 0,
         a -()> b: 1,
@@ -348,12 +345,12 @@
         a: "A",
         b: "B",
         c: "C",
-    ] as NodeId,
+    ],
     [
         ab: a -> b: 1,
         bc: b -> c: -3,
         ca: c -> a: 1,
-    ] as EdgeId
+    ]
 );
 
 #[test]
@@ -367,7 +364,10 @@
     };
 
     assert_eq!(error.current_context(), &FloydWarshallError::NegativeCycle);
-    let participants: HashSet<_> = error.request_ref::<NodeId>().copied().collect();
+    let participants: HashSet<_> = error
+        .request_ref::<NegativeCycle>()
+        .map(|cycle| cycle.node())
+        .collect();
 
     assert_eq!(
         participants,
diff --git a/crates/algorithms/src/shortest_paths/mod.rs b/crates/algorithms/src/shortest_paths/mod.rs
index c199eaf..9137dce 100644
--- a/crates/algorithms/src/shortest_paths/mod.rs
+++ b/crates/algorithms/src/shortest_paths/mod.rs
@@ -38,15 +38,15 @@
 //! use petgraph_dino::DiDinoGraph;
 //!
 //! let mut graph = DiDinoGraph::new();
-//! let a = *graph.insert_node("A").id();
-//! let b = *graph.insert_node("B").id();
-//! graph.insert_edge(7, &a, &b);
+//! let a = graph.insert_node("A").id();
+//! let b = graph.insert_node("B").id();
+//! graph.insert_edge(7, a, b);
 //!
 //! let dijkstra = Dijkstra::directed();
-//! let path = dijkstra.path_between(&graph, &a, &b);
+//! let path = dijkstra.path_between(&graph, a, b);
 //! assert!(path.is_some());
 //!
-//! let path = dijkstra.path_between(&graph, &b, &a);
+//! let path = dijkstra.path_between(&graph, b, a);
 //! assert!(path.is_none());
 //! ```
 
@@ -57,7 +57,7 @@
 pub mod floyd_warshall;
 
 use error_stack::{Context, Result};
-use petgraph_core::{Graph, GraphStorage};
+use petgraph_core::{node::NodeId, Graph, GraphStorage};
 
 pub use self::{
     astar::AStar,
@@ -71,6 +71,18 @@
     floyd_warshall::FloydWarshall,
 };
 
+pub mod utilities {
+    //! # Utilities
+    //!
+    //! This module contains utilities that can be used with shortest path algorithms.
+    //!
+    //! Currently this module only contains the [`cost`] and [`heuristic`] functions, which resolve
+    //! common lifetime problems whenever closures are used for the cost and heuristic functions.
+
+    pub use super::common::closures::{cost, heuristic};
+}
+
+// TODO: should algorithms take `Node<T>` instead?!
 /// # Shortest Path
 ///
 /// A shortest path algorithm is an algorithm that finds a path between two nodes in a graph such
@@ -93,21 +105,21 @@
 /// use petgraph_dino::DiDinoGraph;
 ///
 /// let mut graph = DiDinoGraph::new();
-/// let a = *graph.insert_node("A").id();
-/// let b = *graph.insert_node("B").id();
-/// let c = *graph.insert_node("C").id();
+/// let a = graph.insert_node("A").id();
+/// let b = graph.insert_node("B").id();
+/// let c = graph.insert_node("C").id();
 ///
-/// graph.insert_edge(7, &a, &b);
-/// graph.insert_edge(5, &b, &c);
-/// graph.insert_edge(3, &a, &c);
+/// graph.insert_edge(7, a, b);
+/// graph.insert_edge(5, b, c);
+/// graph.insert_edge(3, a, c);
 ///
 /// let dijkstra = Dijkstra::directed();
-/// let path = dijkstra.path_between(&graph, &a, &c).expect("path exists");
+/// let path = dijkstra.path_between(&graph, a, c).expect("path exists");
 ///
 /// assert_eq!(path.cost().into_value(), 3);
 ///
-/// assert_eq!(path.path().source().id(), &a);
-/// assert_eq!(path.path().target().id(), &c);
+/// assert_eq!(path.path().source().id(), a);
+/// assert_eq!(path.path().target().id(), c);
 ///
 /// assert!(path.path().transit().is_empty());
 /// ```
@@ -128,7 +140,7 @@
     fn path_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = self.every_path(graph)?;
 
@@ -143,7 +155,7 @@
     fn path_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = self.every_path(graph)?;
 
@@ -156,8 +168,8 @@
     fn path_between<'graph>(
         &self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Route<'graph, S, Self::Cost>> {
         self.path_from(graph, source)
             .ok()?
@@ -207,7 +219,7 @@
     fn distance_to<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        target: S::NodeId,
+        target: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = self.every_distance(graph)?;
 
@@ -223,7 +235,7 @@
     fn distance_from<'graph: 'this, 'this>(
         &'this self,
         graph: &'graph Graph<S>,
-        source: S::NodeId,
+        source: NodeId,
     ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
         let iter = self.every_distance(graph)?;
 
@@ -236,8 +248,8 @@
     fn distance_between(
         &self,
         graph: &Graph<S>,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Option<Cost<Self::Cost>> {
         self.every_distance(graph)
             .ok()?
diff --git a/crates/algorithms/tests/shortest_paths/harness.rs b/crates/algorithms/tests/shortest_paths/harness.rs
index 0930eb2..4956ded 100644
--- a/crates/algorithms/tests/shortest_paths/harness.rs
+++ b/crates/algorithms/tests/shortest_paths/harness.rs
@@ -10,12 +10,9 @@
     WalkBuilder,
 };
 use libtest_mimic::Trial;
-use snapbox::{
-    report::{write_diff, Palette},
-    Action, Assert, Data, DataFormat, NormalizeNewlines,
-};
+use snapbox::{report::Palette, Action};
 
-pub struct Harness<S, T> {
+pub(crate) struct Harness<S, T> {
     root: std::path::PathBuf,
     overrides: Option<Override>,
     each: Option<&'static [&'static str]>,
@@ -31,7 +28,7 @@
     S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static,
     T: Fn(&Path, &'static str) -> Result<I, E> + Send + Sync + 'static + Clone,
 {
-    pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
+    pub(crate) fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
         Self {
             root: root.into(),
             overrides: None,
@@ -47,7 +44,7 @@
     /// Path patterns for selecting input files
     ///
     /// This used gitignore syntax
-    pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
+    pub(crate) fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
         let mut overrides = OverrideBuilder::new(&self.root);
         for line in patterns {
             overrides.add(line).unwrap();
@@ -56,20 +53,13 @@
         self
     }
 
-    pub fn each(mut self, names: &'static [&'static str]) -> Self {
+    pub(crate) fn each(mut self, names: &'static [&'static str]) -> Self {
         self.each = Some(names);
         self
     }
 
-    /// Read the failure action from an environment variable
-    pub fn action_env(mut self, var_name: &str) -> Self {
-        let action = Action::with_env_var(var_name);
-        self.action = action.unwrap_or(self.action);
-        self
-    }
-
     /// Override the failure action
-    pub fn action(mut self, action: Action) -> Self {
+    pub(crate) fn action(mut self, action: Action) -> Self {
         self.action = action;
         self
     }
@@ -115,7 +105,7 @@
     }
 
     /// Run tests
-    pub fn test(self) -> ! {
+    pub(crate) fn test(self) -> ! {
         let each = self.each.unwrap_or(&[""]);
 
         let tests = each.iter().flat_map(|name| self.trials(name)).collect();
@@ -364,8 +354,8 @@
     }
 }
 
-pub struct Case {
-    pub name: String,
-    pub fixture: std::path::PathBuf,
-    pub expected: std::path::PathBuf,
+pub(crate) struct Case {
+    pub(crate) name: String,
+    pub(crate) fixture: std::path::PathBuf,
+    pub(crate) expected: std::path::PathBuf,
 }
diff --git a/crates/algorithms/tests/shortest_paths/main.rs b/crates/algorithms/tests/shortest_paths/main.rs
index e2fbafb..90b4cb8 100644
--- a/crates/algorithms/tests/shortest_paths/main.rs
+++ b/crates/algorithms/tests/shortest_paths/main.rs
@@ -10,7 +10,8 @@
 use petgraph_algorithms::shortest_paths::{
     BellmanFord, Dijkstra, FloydWarshall, Route, ShortestPath,
 };
-use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
+use petgraph_core::{edge::EdgeId, node::NodeId};
+use petgraph_dino::{DiDinoGraph, DinoStorage};
 use snapbox::{utils::normalize_lines, Action};
 
 use crate::harness::{Case, Harness};
diff --git a/crates/core/src/attributes.rs b/crates/core/src/attributes.rs
deleted file mode 100644
index 07b75c9..0000000
--- a/crates/core/src/attributes.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-//! Attributes for graph elements.
-//!
-//! This module is used for types that are used to describe the attributes of a graph element on
-//! insertion.
-//!
-//! A consumer is not expected to use this module directly, but instead use the [`From`]
-//! implementations for [`Attributes`].
-//!
-//! This module is exposed to allow for people who like to use a more explicit style to do so and
-//! for the [`NoValue`] type, which cannot be created[^1] and is used to signal that an identifier
-//! is managed by the storage implementation.
-//!
-//! [^1]: See source code for [`NoValue`] if you _really really really_ need to construct it.
-//!
-//! [`GraphStorage::next_node_id`]: crate::storage::GraphStorage::next_node_id
-//! [`GraphStorage::next_edge_id`]: crate::storage::GraphStorage::next_edge_id
-use crate::id::ArbitraryGraphId;
-
-/// Marker type for `GraphId` which are managed by the graph.
-///
-/// This type is used to represent an `id` on insertion and deletion that is unused.
-/// You normally do not need to construct this value directly, as [`Graph::insert_node`] and
-/// [`Graph::insert_edge`] use `Attributes`.
-///
-/// # Implementation Details
-///
-/// The existence of this type is under stability guarantee, meaning that it will only be removed or
-/// renamed according to `SemVer`, but the internals, such as layout or size, are not.
-/// This includes the construction method.
-///
-/// [`Graph::insert_node`]: crate::graph::Graph::insert_node
-/// [`Graph::insert_edge`]: crate::graph::Graph::insert_edge
-pub struct NoValue(());
-
-impl NoValue {
-    /// Construct a new `NoValue`.
-    ///
-    /// This is only available for testing purposes.
-    #[doc(hidden)]
-    #[must_use]
-    pub const fn new() -> Self {
-        Self(())
-    }
-}
-
-/// Attributes for graph elements.
-///
-/// This type is used to represent the attributes of a graph element on insertion.
-///
-/// This type is completely opaque and is only used internally in the [`Graph`] implementation to
-/// allow for transparent insertion using [`From`] implementations for elements that require either
-/// of the types of `id`: [`ManagedGraphId`] or [`ArbitraryGraphId`].
-///
-/// You shouldn't need to construct this type directly, but instead simply use the [`From`]
-/// implementation via `graph.insert_node(<weight>)` or `graph.insert_node((<weight>,))` for a
-/// [`ManagedGraphId`] or `graph.insert_node((<id>, <weight>))` for an [`ArbitraryGraphId`].
-/// This also applies for `insert_edge`.
-///
-/// [`Graph`]: crate::graph::Graph
-/// [`ManagedGraphId`]: crate::id::ManagedGraphId
-pub struct Attributes<I, W> {
-    pub(crate) id: I,
-    pub(crate) weight: W,
-}
-
-impl<W> Attributes<NoValue, W> {
-    /// Construct a new `Attributes` with the given weight.
-    ///
-    /// This will not set the `id` of the attributes, and can only be used for graphs where the `id`
-    /// of the element must be a [`ManagedGraphId`].
-    ///
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    pub const fn new(weight: W) -> Self {
-        Self {
-            id: NoValue(()),
-            weight,
-        }
-    }
-}
-
-impl<W> Attributes<NoValue, W> {
-    /// Set the `id` of the attributes.
-    ///
-    /// This will return a new `Attributes` with the given `id`, converting it from attributes that
-    /// are only valid for elements that have a [`ManagedGraphId`] as their `id`, to ones that only
-    /// have an [`ArbitraryGraphId`].
-    ///
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    pub fn with_id<I>(self, id: impl Into<I>) -> Attributes<I, W>
-    where
-        I: ArbitraryGraphId,
-    {
-        Attributes {
-            id: id.into(),
-            weight: self.weight,
-        }
-    }
-}
-
-impl<I, J, W> From<(J, W)> for Attributes<I, W>
-where
-    I: From<J> + ArbitraryGraphId,
-{
-    fn from(value: (J, W)) -> Self {
-        Self {
-            id: I::from(value.0),
-            weight: value.1,
-        }
-    }
-}
-
-impl<W> From<(W,)> for Attributes<NoValue, W> {
-    fn from((weight,): (W,)) -> Self {
-        Self {
-            id: NoValue(()),
-            weight,
-        }
-    }
-}
-
-impl<W> From<W> for Attributes<NoValue, W> {
-    fn from(weight: W) -> Self {
-        Self {
-            id: NoValue(()),
-            weight,
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use core::{fmt::Debug, marker::PhantomData};
-
-    use crate::{
-        attributes::{Attributes, NoValue},
-        ArbitraryGraphId, GraphId, ManagedGraphId,
-    };
-
-    #[derive(Debug, Copy, Clone, PartialEq)]
-    struct Managed(usize);
-
-    impl GraphId for Managed {
-        type AttributeIndex = NoValue;
-    }
-
-    impl ManagedGraphId for Managed {}
-
-    #[derive(Debug, Copy, Clone, PartialEq)]
-    struct Arbitrary<V>(V);
-
-    impl<V> GraphId for Arbitrary<V>
-    where
-        V: Debug + Copy + Clone + PartialEq,
-    {
-        type AttributeIndex = Self;
-    }
-
-    impl<V> ArbitraryGraphId for Arbitrary<V> where V: Debug + Copy + Clone + PartialEq {}
-
-    impl<T> From<T> for Arbitrary<T> {
-        fn from(value: T) -> Self {
-            Self(value)
-        }
-    }
-
-    struct Fake<T> {
-        _marker: PhantomData<T>,
-    }
-
-    impl<T> Fake<T>
-    where
-        T: GraphId,
-    {
-        fn invoke(attributes: impl Into<Attributes<T::AttributeIndex, usize>>) {
-            let _attr = attributes.into();
-        }
-    }
-
-    #[test]
-    fn from() {
-        Fake::<Managed>::invoke(2usize);
-
-        Fake::<Arbitrary<usize>>::invoke((2usize, 2usize));
-    }
-}
diff --git a/crates/core/src/edge/compat.rs b/crates/core/src/edge/compat.rs
index 18b9892..4e578c8 100644
--- a/crates/core/src/edge/compat.rs
+++ b/crates/core/src/edge/compat.rs
@@ -1,24 +1,27 @@
 //! Compatability implementations for deprecated graph traits.
 #![allow(deprecated)]
 
-use crate::{deprecated::visit::EdgeRef, edge::Edge, storage::GraphStorage};
+use crate::{
+    deprecated::visit::EdgeRef,
+    edge::{Edge, EdgeId},
+    node::NodeId,
+    storage::GraphStorage,
+};
 
 impl<S> EdgeRef for Edge<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Clone,
-    S::EdgeId: Clone,
 {
-    type EdgeId = S::EdgeId;
-    type NodeId = S::NodeId;
+    type EdgeId = EdgeId;
+    type NodeId = NodeId;
     type Weight = S::EdgeWeight;
 
     fn source(&self) -> Self::NodeId {
-        self.u.clone()
+        self.u
     }
 
     fn target(&self) -> Self::NodeId {
-        self.v.clone()
+        self.v
     }
 
     fn weight(&self) -> &Self::Weight {
@@ -26,6 +29,6 @@
     }
 
     fn id(&self) -> Self::EdgeId {
-        self.id.clone()
+        self.id
     }
 }
diff --git a/crates/core/src/edge/mod.rs b/crates/core/src/edge/mod.rs
index 5ca357b..1415612 100644
--- a/crates/core/src/edge/mod.rs
+++ b/crates/core/src/edge/mod.rs
@@ -21,16 +21,87 @@
 mod direction;
 pub mod marker;
 
-use core::fmt::{Debug, Formatter};
+use core::fmt::{Debug, Display, Formatter};
 
 pub use self::{direction::Direction, marker::GraphDirectionality};
-use crate::{node::Node, storage::GraphStorage, DirectedGraphStorage};
+use crate::{
+    node::{Node, NodeId},
+    storage::GraphStorage,
+    DirectedGraphStorage,
+};
 
-type DetachedStorageEdge<S> = DetachedEdge<
-    <S as GraphStorage>::EdgeId,
-    <S as GraphStorage>::NodeId,
-    <S as GraphStorage>::EdgeWeight,
->;
+type DetachedStorageEdge<S> = DetachedEdge<<S as GraphStorage>::EdgeWeight>;
+
+/// ID of an edge in a graph.
+///
+/// This is guaranteed to be unique within the graph, library authors and library consumers **must**
+/// treat this as an opaque type akin to [`TypeId`].
+///
+/// The layout of the type is semver stable, but not part of the public API.
+///
+/// [`GraphStorage`] implementations may uphold additional invariants on the inner value and
+/// code outside of the [`GraphStorage`] should **never** construct a [`EdgeId`] directly.
+///
+/// Accessing a [`GraphStorage`] implementation with a [`EdgeId`] not returned by an instance itself
+/// is considered undefined behavior.
+///
+/// [`TypeId`]: core::any::TypeId
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct EdgeId(usize);
+
+impl Display for EdgeId {
+    // we could also utilize a VTable here instead, that would allow for custom formatting
+    // but that would be an additional pointer added to the type that must be carried around
+    // that's about ~8 bytes on 64-bit systems
+    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+        write!(f, "EdgeId({})", self.0)
+    }
+}
+
+// TODO: find a better way to gate these functions
+impl EdgeId {
+    /// Creates a new [`EdgeId`].
+    ///
+    /// # Note
+    ///
+    /// Using this outside of the [`GraphStorage`] implementation is considered undefined behavior.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use petgraph_core::edge::EdgeId;
+    ///
+    /// let id = EdgeId::new(0);
+    /// ```
+    // Hidden so that non-GraphStorage implementors are not tempted to use this.
+    #[doc(hidden)]
+    #[must_use]
+    pub const fn new(id: usize) -> Self {
+        Self(id)
+    }
+
+    /// Returns the inner value of the [`EdgeId`].
+    ///
+    /// # Note
+    ///
+    /// Using this outside of the [`GraphStorage`] implementation is considered undefined behavior.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use petgraph_core::edge::EdgeId;
+    ///
+    /// let id = EdgeId::new(0);
+    ///
+    /// assert_eq!(id.into_inner(), 0);
+    /// ```
+    // Hidden so that non-GraphStorage implementors are not tempted to use this.
+    #[doc(hidden)]
+    #[must_use]
+    pub const fn into_inner(self) -> usize {
+        self.0
+    }
+}
 
 /// Active edge in the graph.
 ///
@@ -64,10 +135,10 @@
 {
     storage: &'a S,
 
-    id: S::EdgeId,
+    id: EdgeId,
 
-    u: S::NodeId,
-    v: S::NodeId,
+    u: NodeId,
+    v: NodeId,
 
     weight: &'a S::EdgeWeight,
 }
@@ -151,11 +222,11 @@
     pub fn new(
         storage: &'a S,
 
-        id: S::EdgeId,
+        id: EdgeId,
         weight: &'a S::EdgeWeight,
 
-        u: S::NodeId,
-        v: S::NodeId,
+        u: NodeId,
+        v: NodeId,
     ) -> Self {
         debug_assert!(storage.contains_node(u));
         debug_assert!(storage.contains_node(v));
@@ -195,7 +266,7 @@
     /// assert_eq!(edge.id(), &aa);
     /// ```
     #[must_use]
-    pub const fn id(&self) -> S::EdgeId {
+    pub const fn id(&self) -> EdgeId {
         self.id
     }
 
@@ -230,7 +301,7 @@
     /// assert!((u, v) == (&a, &b) || (u, v) == (&b, &a));
     /// ```
     #[must_use]
-    pub const fn endpoint_ids(&self) -> (S::NodeId, S::NodeId) {
+    pub const fn endpoint_ids(&self) -> (NodeId, NodeId) {
         (self.u, self.v)
     }
 
@@ -309,6 +380,33 @@
     pub const fn weight(&self) -> &'a S::EdgeWeight {
         self.weight
     }
+
+    /// Change the underlying storage of this edge.
+    ///
+    /// Should only be used when layering multiple [`GraphStorage`] implementations on top of each
+    /// other.
+    ///
+    /// # Note
+    ///
+    /// This should not lead to any undefined behaviour, but might have unintended consequences if
+    /// the storage does not recognize the inner id as valid.
+    /// You should only use this if you know what you are doing.
+    #[must_use]
+    pub const fn change_storage_unchecked<T>(self, storage: &'a T) -> Edge<'a, T>
+    where
+        T: GraphStorage<EdgeWeight = S::EdgeWeight>,
+    {
+        Edge {
+            storage,
+
+            id: self.id,
+
+            u: self.u,
+            v: self.v,
+
+            weight: self.weight,
+        }
+    }
 }
 
 impl<'a, S> Edge<'a, S>
@@ -336,7 +434,7 @@
     /// assert_eq!(edge.source_id(), &a);
     /// ```
     #[must_use]
-    pub const fn source_id(&self) -> S::NodeId {
+    pub const fn source_id(&self) -> NodeId {
         self.u
     }
 
@@ -401,7 +499,7 @@
     /// let edge = graph.edge(&ab).unwrap();
     /// assert_eq!(edge.target_id(), &b);
     /// ```
-    pub const fn target_id(&self) -> S::NodeId {
+    pub const fn target_id(&self) -> NodeId {
         self.v
     }
 
@@ -518,12 +616,12 @@
 where
     S: GraphStorage,
 {
-    id: S::EdgeId,
+    id: EdgeId,
 
     weight: &'a mut S::EdgeWeight,
 
-    u: S::NodeId,
-    v: S::NodeId,
+    u: NodeId,
+    v: NodeId,
 }
 
 impl<'a, S> EdgeMut<'a, S>
@@ -547,7 +645,7 @@
     ///
     /// [`Graph::edge_mut`]: crate::graph::Graph::edge_mut
     /// [`Graph::insert_edge`]: crate::graph::Graph::insert_edge
-    pub fn new(id: S::EdgeId, weight: &'a mut S::EdgeWeight, u: S::NodeId, v: S::NodeId) -> Self {
+    pub fn new(id: EdgeId, weight: &'a mut S::EdgeWeight, u: NodeId, v: NodeId) -> Self {
         Self { id, weight, u, v }
     }
 
@@ -572,7 +670,7 @@
     /// assert_eq!(edge.id(), &ab);
     /// ```
     #[must_use]
-    pub const fn id(&self) -> S::EdgeId {
+    pub const fn id(&self) -> EdgeId {
         self.id
     }
 
@@ -605,7 +703,7 @@
     /// assert!((u, v) == (&a, &b) || (u, v) == (&b, &a));
     /// ```
     #[must_use]
-    pub const fn endpoint_ids(&self) -> (S::NodeId, S::NodeId) {
+    pub const fn endpoint_ids(&self) -> (NodeId, NodeId) {
         (self.u, self.v)
     }
 
@@ -651,6 +749,31 @@
     pub fn weight_mut(&mut self) -> &mut S::EdgeWeight {
         self.weight
     }
+
+    /// Change the underlying storage of this edge.
+    ///
+    /// Should only be used when layering multiple [`GraphStorage`] implementations on top of each
+    /// other.
+    ///
+    /// # Note
+    ///
+    /// This should not lead to any undefined behaviour, but might have unintended consequences if
+    /// the storage does not recognize the inner id as valid.
+    /// You should only use this if you know what you are doing.
+    #[must_use]
+    pub fn change_storage_unchecked<T>(self) -> EdgeMut<'a, T>
+    where
+        T: GraphStorage<EdgeWeight = S::EdgeWeight>,
+    {
+        EdgeMut {
+            id: self.id,
+
+            weight: self.weight,
+
+            u: self.u,
+            v: self.v,
+        }
+    }
 }
 
 impl<'a, S> EdgeMut<'a, S>
@@ -675,7 +798,7 @@
     /// assert_eq!(edge.source_id(), &a);
     /// ```
     #[must_use]
-    pub const fn source_id(&self) -> S::NodeId {
+    pub const fn source_id(&self) -> NodeId {
         self.u
     }
 
@@ -697,7 +820,7 @@
     /// assert_eq!(edge.target_id(), &b);
     /// ```
     #[must_use]
-    pub const fn target_id(&self) -> S::NodeId {
+    pub const fn target_id(&self) -> NodeId {
         self.v
     }
 }
@@ -771,20 +894,20 @@
 /// [`Graph::into_parts`]: crate::graph::Graph::into_parts
 /// [`Graph::from_parts`]: crate::graph::Graph::from_parts
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct DetachedEdge<E, N, W> {
+pub struct DetachedEdge<W> {
     /// The unique id of the edge.
-    pub id: E,
+    pub id: EdgeId,
 
     /// The `u` endpoint of the `(u, v)` pair of endpoints.
-    pub u: N,
+    pub u: NodeId,
     /// The `v` endpoint of the `(u, v)` pair of endpoints.
-    pub v: N,
+    pub v: NodeId,
 
     /// The weight of the edge.
     pub weight: W,
 }
 
-impl<E, N, W> DetachedEdge<E, N, W> {
+impl<W> DetachedEdge<W> {
     /// Create a new detached edge.
     ///
     /// In an undirected graph `u` and `v` are interchangeable, but in a directed graph (implements
@@ -797,7 +920,7 @@
     ///
     /// let edge = DetachedEdge::new(0, "A → B", 1, 2);
     /// ```
-    pub const fn new(id: E, weight: W, u: N, v: N) -> Self {
+    pub const fn new(id: EdgeId, weight: W, u: NodeId, v: NodeId) -> Self {
         Self { id, u, v, weight }
     }
 }
diff --git a/crates/core/src/graph/compat.rs b/crates/core/src/graph/compat.rs
index b5f997e..c0aa4c2 100644
--- a/crates/core/src/graph/compat.rs
+++ b/crates/core/src/graph/compat.rs
@@ -4,33 +4,32 @@
 #[cfg(feature = "alloc")]
 use alloc::vec::Vec;
 
-use bitvec::{boxed::BitBox, vec::BitVec};
-
 #[cfg(feature = "alloc")]
 use crate::deprecated::data::{Build, Create, DataMap, FromElements};
 #[cfg(feature = "fixedbitset")]
 use crate::deprecated::visit::GetAdjacencyMatrix;
 use crate::{
     deprecated::visit::{
-        Data, EdgeCount, EdgeIndexable, FilterEdge, FilterNode, GraphBase, IntoEdgeReferences,
-        IntoEdges, IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers,
+        Data, EdgeCount, EdgeIndexable, FilterNode, GraphBase, IntoEdgeReferences, IntoEdges,
+        IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers,
         IntoNodeReferences, NodeCompactIndexable, NodeCount, NodeIndexable, VisitMap, Visitable,
     },
-    edge::{Direction, Edge},
+    edge::{Direction, Edge, EdgeId},
     graph::Graph,
-    id::{IndexMapper, LinearGraphId, ManagedGraphId},
-    node::Node,
-    storage::{DirectedGraphStorage, GraphStorage},
+    node::{Node, NodeId},
+    storage::{
+        auxiliary::{BooleanGraphStorage, Hints},
+        sequential::GraphIdBijection,
+        DirectedGraphStorage, GraphStorage,
+    },
 };
 
 impl<S> GraphBase for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
-    type EdgeId = S::EdgeId;
-    type NodeId = S::NodeId;
+    type EdgeId = EdgeId;
+    type NodeId = NodeId;
 }
 
 // TODO: GraphProp?!
@@ -38,8 +37,6 @@
 impl<S> NodeCount for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     fn node_count(&self) -> usize {
         self.num_nodes()
@@ -49,37 +46,28 @@
 impl<S> NodeIndexable for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Copy,
-    S::EdgeId: Copy,
 {
     fn node_bound(&self) -> usize {
         self.num_nodes()
     }
 
     fn to_index(&self, a: Self::NodeId) -> usize {
-        S::NodeId::index_mapper(&self.storage).index(a)
+        self.storage.node_id_bijection().index(a)
     }
 
     fn from_index(&self, i: usize) -> Self::NodeId {
-        S::NodeId::index_mapper(&self.storage)
+        self.storage
+            .node_id_bijection()
             .reverse(i)
             .expect("unable to determine index")
     }
 }
 
-impl<S> NodeCompactIndexable for Graph<S>
-where
-    S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Copy,
-    S::EdgeId: Copy,
-{
-}
+impl<S> NodeCompactIndexable for Graph<S> where S: GraphStorage {}
 
 impl<S> EdgeCount for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     fn edge_count(&self) -> usize {
         self.num_edges()
@@ -89,19 +77,18 @@
 impl<S> EdgeIndexable for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: LinearGraphId<S> + Copy,
 {
     fn edge_bound(&self) -> usize {
         self.num_edges()
     }
 
     fn to_index(&self, a: Self::EdgeId) -> usize {
-        S::EdgeId::index_mapper(&self.storage).index(a)
+        self.storage.edge_id_bijection().index(a)
     }
 
     fn from_index(&self, i: usize) -> Self::EdgeId {
-        S::EdgeId::index_mapper(&self.storage)
+        self.storage
+            .edge_id_bijection()
             .reverse(i)
             .expect("unable to determine index")
     }
@@ -110,8 +97,6 @@
 impl<S> Data for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type EdgeWeight = S::EdgeWeight;
     type NodeWeight = S::NodeWeight;
@@ -120,8 +105,6 @@
 impl<S> IntoNodeIdentifiers for &Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type NodeIdentifiers = impl Iterator<Item = Self::NodeId>;
 
@@ -133,8 +116,6 @@
 impl<'a, S> IntoNodeReferences for &'a Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type NodeRef = Node<'a, S>;
 
@@ -148,8 +129,6 @@
 impl<'a, S> IntoEdgeReferences for &'a Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type EdgeRef = Edge<'a, S>;
 
@@ -164,8 +143,6 @@
 impl<'a, S> IntoNeighbors for &'a Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type Neighbors = impl Iterator<Item = Self::NodeId>;
 
@@ -185,8 +162,6 @@
 impl<'a, S> IntoNeighborsDirected for &'a Graph<S>
 where
     S: DirectedGraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type NeighborsDirected = impl Iterator<Item = Self::NodeId>;
 
@@ -203,8 +178,6 @@
 impl<'a, S> IntoEdges for &'a Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type Edges = impl Iterator<Item = Self::EdgeRef>;
 
@@ -218,8 +191,6 @@
 impl<'a, S> IntoEdgesDirected for &'a Graph<S>
 where
     S: DirectedGraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type EdgesDirected = impl Iterator<Item = Self::EdgeRef>;
 
@@ -231,84 +202,45 @@
     }
 }
 
-pub struct VisitationMap<'a, S, T>
+pub struct NodeVisitationMap<'a, S>
 where
     S: GraphStorage + 'a,
-    T: LinearGraphId<S> + Clone + 'a,
 {
-    inner: BitBox,
-    mapper: <T as LinearGraphId<S>>::Mapper<'a>,
+    inner: S::BooleanNodeStorage<'a>,
 }
 
-impl<'a, S> VisitationMap<'a, S, S::NodeId>
+impl<'a, S> VisitMap<NodeId> for NodeVisitationMap<'a, S>
 where
     S: GraphStorage + 'a,
-    S::NodeId: LinearGraphId<S> + Clone,
 {
-    fn new_node(size: usize, mapper: <S::NodeId as LinearGraphId<S>>::Mapper<'a>) -> Self {
-        Self {
-            inner: BitVec::repeat(false, size).into_boxed_bitslice(),
-            mapper,
-        }
+    fn visit(&mut self, a: NodeId) -> bool {
+        self.inner.set(a, true).is_none()
+    }
+
+    fn is_visited(&self, a: &NodeId) -> bool {
+        self.inner.get(*a).unwrap_or(false)
     }
 }
 
-impl<'a, S, T> VisitMap<T> for VisitationMap<'a, S, T>
+impl<'a, S> FilterNode<NodeId> for NodeVisitationMap<'a, S>
 where
     S: GraphStorage + 'a,
-    T: LinearGraphId<S> + Clone,
 {
-    fn visit(&mut self, a: T) -> bool {
-        let Some(index) = self.mapper.get(a) else {
-            return false;
-        };
-
-        !self.inner.replace(index, true)
-    }
-
-    fn is_visited(&self, a: &T) -> bool {
-        let Some(index) = self.mapper.get(*a) else {
-            return false;
-        };
-
-        let Some(bit) = self.inner.get(index) else {
-            return false;
-        };
-
-        *bit
-    }
-}
-
-impl<'a, S> FilterNode<S::NodeId> for VisitationMap<'a, S, S::NodeId>
-where
-    S: GraphStorage + 'a,
-    S::NodeId: LinearGraphId<S> + Clone,
-{
-    fn include_node(&self, node: S::NodeId) -> bool {
+    fn include_node(&self, node: NodeId) -> bool {
         self.is_visited(&node)
     }
 }
 
-impl<'a, S> FilterEdge<S::EdgeId> for VisitationMap<'a, S, S::EdgeId>
-where
-    S: GraphStorage + 'a,
-    S::EdgeId: LinearGraphId<S> + Clone,
-{
-    fn include_edge(&self, edge: S::EdgeId) -> bool {
-        self.is_visited(&edge)
-    }
-}
-
 impl<'a, S> Visitable for &'a Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: LinearGraphId<S> + Copy,
-    S::EdgeId: Copy,
 {
-    type Map = VisitationMap<'a, S, S::NodeId>;
+    type Map = NodeVisitationMap<'a, S>;
 
     fn visit_map(&self) -> Self::Map {
-        VisitationMap::new_node(self.num_nodes(), S::NodeId::index_mapper(&self.storage))
+        NodeVisitationMap {
+            inner: self.storage.boolean_node_storage(Hints::default()),
+        }
     }
 
     fn reset_map(&self, map: &mut Self::Map) {
@@ -320,14 +252,12 @@
 impl<S> GetAdjacencyMatrix for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     type AdjMatrix = ();
 
     fn adjacency_matrix(&self) -> Self::AdjMatrix {}
 
-    fn is_adjacent(&self, _: &Self::AdjMatrix, a: Self::NodeId, b: Self::NodeId) -> bool {
+    fn is_adjacent(&self, (): &Self::AdjMatrix, a: Self::NodeId, b: Self::NodeId) -> bool {
         self.edges_between(a, b).next().is_some()
     }
 }
@@ -336,8 +266,6 @@
 impl<S> DataMap for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: Copy,
-    S::EdgeId: Copy,
 {
     fn node_weight(&self, id: Self::NodeId) -> Option<&Self::NodeWeight> {
         self.node(id).map(|node| node.weight())
@@ -352,8 +280,6 @@
 impl<S> Build for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
 {
     fn add_node(&mut self, weight: Self::NodeWeight) -> Self::NodeId {
         self.insert_node(weight).id()
@@ -373,8 +299,6 @@
 impl<S> Create for Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
 {
     fn with_capacity(nodes: usize, edges: usize) -> Self {
         Self::with_capacity(Some(nodes), Some(edges))
@@ -382,10 +306,4 @@
 }
 
 #[cfg(feature = "alloc")]
-impl<S> FromElements for Graph<S>
-where
-    S: GraphStorage,
-    S::NodeId: ManagedGraphId + Copy,
-    S::EdgeId: ManagedGraphId + Copy,
-{
-}
+impl<S> FromElements for Graph<S> where S: GraphStorage {}
diff --git a/crates/core/src/graph/directed.rs b/crates/core/src/graph/directed.rs
index 7e8122d..fafa5ee 100644
--- a/crates/core/src/graph/directed.rs
+++ b/crates/core/src/graph/directed.rs
@@ -1,7 +1,7 @@
 use crate::{
     edge::{Direction, Edge, EdgeMut},
     graph::Graph,
-    node::{Node, NodeMut},
+    node::{Node, NodeId, NodeMut},
     storage::DirectedGraphStorage,
 };
 
@@ -46,8 +46,8 @@
     /// ```
     pub fn directed_edges_between(
         &self,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = Edge<'_, S>> {
         self.storage.directed_edges_between(source, target)
     }
@@ -89,8 +89,8 @@
     /// ```
     pub fn directed_edges_between_mut(
         &mut self,
-        source: S::NodeId,
-        target: S::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = EdgeMut<'_, S>> {
         self.storage.directed_edges_between_mut(source, target)
     }
@@ -108,7 +108,7 @@
     #[inline]
     pub fn neighbors_directed(
         &self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Node<'_, S>> {
         self.neighbours_directed(id, direction)
@@ -156,7 +156,7 @@
     /// ```
     pub fn neighbours_directed(
         &self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Node<'_, S>> {
         self.storage.node_directed_neighbours(id, direction)
@@ -176,7 +176,7 @@
     #[inline]
     pub fn neighbors_directed_mut(
         &mut self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = NodeMut<'_, S>> {
         self.neighbours_directed_mut(id, direction)
@@ -221,7 +221,7 @@
     /// ```
     pub fn neighbours_directed_mut(
         &mut self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = NodeMut<'_, S>> {
         self.storage.node_directed_neighbours_mut(id, direction)
@@ -263,7 +263,7 @@
     /// ```
     pub fn connections_directed(
         &self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Edge<'_, S>> {
         self.storage.node_directed_connections(id, direction)
@@ -310,7 +310,7 @@
     /// ```
     pub fn connections_directed_mut(
         &mut self,
-        id: S::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = EdgeMut<'_, S>> {
         self.storage.node_directed_connections_mut(id, direction)
diff --git a/crates/core/src/graph/insert.rs b/crates/core/src/graph/insert.rs
index 48ce252..c1035b8 100644
--- a/crates/core/src/graph/insert.rs
+++ b/crates/core/src/graph/insert.rs
@@ -1,11 +1,12 @@
+#[cfg(feature = "alloc")]
+use alloc::{vec, vec::Vec};
+
 use error_stack::{Result, ResultExt};
 
 use crate::{
-    attributes::{Attributes, NoValue},
-    edge::EdgeMut,
+    edge::{EdgeId, EdgeMut},
     graph::Graph,
-    id::{ArbitraryGraphId, GraphId, ManagedGraphId},
-    node::NodeMut,
+    node::{NodeId, NodeMut},
     storage::GraphStorage,
     Error,
 };
@@ -35,13 +36,8 @@
     /// This may include things like parallel edges or self loops depending on implementation.
     ///
     /// Refer to the documentation of the underlying storage for more information.
-    pub fn try_insert_node(
-        &mut self,
-        attributes: impl Into<Attributes<<S::NodeId as GraphId>::AttributeIndex, S::NodeWeight>>,
-    ) -> Result<NodeMut<S>, Error> {
-        let Attributes { id, weight } = attributes.into();
-
-        let id = self.storage.next_node_id(id);
+    pub fn try_insert_node(&mut self, weight: S::NodeWeight) -> Result<NodeMut<S>, Error> {
+        let id = self.storage.next_node_id();
         self.storage.insert_node(id, weight).change_context(Error)
     }
 
@@ -71,7 +67,7 @@
     /// The reason is that some storage types might not be able to guarantee that the node can be
     /// inserted, but we still want to provide a convenient way to insert a node.
     /// Another reason is that this mirrors the API of other libraries and the std, such as the
-    /// standard library (through [`alloc::vec::Vec::push`], or
+    /// standard library (through [`Vec::push`], or
     /// [`std::collections::HashMap::insert`]).
     /// These may also panic and do not return a result.
     /// But(!) it is important to note that the constraints and reason why they may panic are quite
@@ -87,11 +83,8 @@
     /// This may include things like parallel edges or self loops depending on implementation.
     ///
     /// Refer to the documentation of the underlying storage for more information.
-    pub fn insert_node(
-        &mut self,
-        attributes: impl Into<Attributes<<S::NodeId as GraphId>::AttributeIndex, S::NodeWeight>>,
-    ) -> NodeMut<S> {
-        self.try_insert_node(attributes)
+    pub fn insert_node(&mut self, weight: S::NodeWeight) -> NodeMut<S> {
+        self.try_insert_node(weight)
             .expect("Constraint violation. Try using `try_insert_node` instead.")
     }
 }
@@ -99,7 +92,6 @@
 impl<S> Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: ManagedGraphId,
 {
     /// Insert a node, where the weight is dependent on the id.
     ///
@@ -138,9 +130,9 @@
     /// Refer to the documentation of the underlying storage for more information.
     pub fn try_insert_node_with(
         &mut self,
-        weight: impl FnOnce(S::NodeId) -> S::NodeWeight,
+        weight: impl FnOnce(NodeId) -> S::NodeWeight,
     ) -> Result<NodeMut<S>, Error> {
-        let id = self.storage.next_node_id(NoValue::new());
+        let id = self.storage.next_node_id();
         let weight = weight(id);
 
         self.storage.insert_node(id, weight).change_context(Error)
@@ -178,10 +170,7 @@
     /// This may include things like parallel edges or self loops depending on implementation.
     ///
     /// Refer to the documentation of the underlying storage for more information.
-    pub fn insert_node_with(
-        &mut self,
-        weight: impl FnOnce(S::NodeId) -> S::NodeWeight,
-    ) -> NodeMut<S> {
+    pub fn insert_node_with(&mut self, weight: impl FnOnce(NodeId) -> S::NodeWeight) -> NodeMut<S> {
         self.try_insert_node_with(weight)
             .expect("Constraint violation. Try using `try_insert_node_with` instead.")
     }
@@ -190,60 +179,6 @@
 impl<S> Graph<S>
 where
     S: GraphStorage,
-    S::NodeId: ArbitraryGraphId,
-{
-    /// Insert a node, or update the weight of an existing node.
-    ///
-    /// This is the fallible version of [`Self::upsert_node`].
-    // TODO: Example
-    /// # Errors
-    ///
-    /// The same errors as [`Self::try_insert_node`] may occur.
-    ///
-    /// # Panics
-    ///
-    /// If the storage is inconsistent.
-    /// This should never happen, as this is an implementation error of the underlying
-    /// [`GraphStorage`], which must guarantee that if one calls [`GraphStorage::contains_node`]
-    /// with the id of the node to check if a node exists, and then calls
-    /// [`GraphStorage::node_mut`] with the same id, it must return a node.
-    pub fn try_upsert_node(
-        &mut self,
-        id: S::NodeId,
-        weight: S::NodeWeight,
-    ) -> Result<NodeMut<S>, Error> {
-        // we cannot use `if let` here due to limitations of the borrow checker
-        if self.storage.contains_node(id) {
-            let mut node = self
-                .storage
-                .node_mut(id)
-                .expect("inconsistent storage, node must exist");
-
-            *node.weight_mut() = weight;
-
-            Ok(node)
-        } else {
-            self.storage.insert_node(id, weight).change_context(Error)
-        }
-    }
-
-    /// Insert a node, or update the weight of an existing node.
-    ///
-    /// This is the infallible version of [`Self::try_upsert_node`], which will panic instead.
-    // TODO: Example
-    /// # Panics
-    ///
-    /// The same panics as [`Self::try_upsert_node`] may occur, as well as the ones from
-    /// [`Self::insert_node`].
-    pub fn upsert_node(&mut self, id: S::NodeId, weight: S::NodeWeight) -> NodeMut<S> {
-        self.try_upsert_node(id, weight)
-            .expect("Constraint violation. Try using `try_upsert_node` instead.")
-    }
-}
-
-impl<S> Graph<S>
-where
-    S: GraphStorage,
 {
     /// Try to insert an edge with the given attributes.
     ///
@@ -271,13 +206,11 @@
     /// Refer to the documentation of the underlying storage for more information.
     pub fn try_insert_edge(
         &mut self,
-        attributes: impl Into<Attributes<<S::EdgeId as GraphId>::AttributeIndex, S::EdgeWeight>>,
-        source: S::NodeId,
-        target: S::NodeId,
+        weight: S::EdgeWeight,
+        source: NodeId,
+        target: NodeId,
     ) -> Result<EdgeMut<S>, Error> {
-        let Attributes { id, weight } = attributes.into();
-
-        let id = self.storage.next_edge_id(id);
+        let id = self.storage.next_edge_id();
         self.storage
             .insert_edge(id, weight, source, target)
             .change_context(Error)
@@ -311,11 +244,11 @@
     /// This may include things like parallel edges or self loops depending on implementation.
     pub fn insert_edge(
         &mut self,
-        attributes: impl Into<Attributes<<S::EdgeId as GraphId>::AttributeIndex, S::EdgeWeight>>,
-        source: S::NodeId,
-        target: S::NodeId,
+        weight: S::EdgeWeight,
+        source: NodeId,
+        target: NodeId,
     ) -> EdgeMut<S> {
-        self.try_insert_edge(attributes, source, target)
+        self.try_insert_edge(weight, source, target)
             .expect("Constraint violation. Try using `try_insert_edge` instead.")
     }
 }
@@ -323,7 +256,6 @@
 impl<S> Graph<S>
 where
     S: GraphStorage,
-    S::EdgeId: ManagedGraphId,
 {
     /// Insert an edge, where the weight is dependent on the id.
     ///
@@ -361,11 +293,11 @@
     /// The same errors as [`Self::try_insert_edge`] may occur.
     pub fn try_insert_edge_with(
         &mut self,
-        weight: impl FnOnce(S::EdgeId) -> S::EdgeWeight,
-        source: S::NodeId,
-        target: S::NodeId,
+        weight: impl FnOnce(EdgeId) -> S::EdgeWeight,
+        source: NodeId,
+        target: NodeId,
     ) -> Result<EdgeMut<S>, Error> {
-        let id = self.storage.next_edge_id(NoValue::new());
+        let id = self.storage.next_edge_id();
         let weight = weight(id);
 
         self.storage
@@ -406,62 +338,63 @@
     /// The same panics as [`Self::insert_edge`] may occur.
     pub fn insert_edge_with(
         &mut self,
-        weight: impl FnOnce(S::EdgeId) -> S::EdgeWeight,
-        source: S::NodeId,
-        target: S::NodeId,
+        weight: impl FnOnce(EdgeId) -> S::EdgeWeight,
+        source: NodeId,
+        target: NodeId,
     ) -> EdgeMut<S> {
         self.try_insert_edge_with(weight, source, target)
             .expect("Constraint violation. Try using `try_insert_edge_with` instead.")
     }
 }
 
+#[cfg(feature = "alloc")]
 impl<S> Graph<S>
 where
     S: GraphStorage,
-    S::EdgeId: ArbitraryGraphId,
+    S::EdgeWeight: Clone,
 {
     /// Insert an edge, or update the weight of an existing edge, if it exists.
     ///
+    /// If multiple edges exist between the given nodes, all of them will be updated with the given
+    ///
+    /// Edges are treated as undirected, so the order of the nodes does not matter.
+    ///
+    /// Unlike [`Self::try_upsert_edge`], this will not return the edge, and instead return a list
+    /// containing all affected edge identifiers.
+    ///
     /// This is the fallible version of [`Self::upsert_edge`].
     // TODO: Example
     ///
     /// # Errors
     ///
     /// The same errors as [`Self::try_insert_edge`] may occur.
-    ///
-    /// # Panics
-    ///
-    /// If the storage is inconsistent.
-    /// This should never happen, as this is an implementation error of the underlying
-    /// [`GraphStorage`], which must guarantee that if one calls [`GraphStorage::contains_edge`]
-    /// with the id of the edge to check if an edge exists, and then calls
-    /// [`GraphStorage::edge_mut`] with the same id, it must return an edge.
     pub fn try_upsert_edge(
         &mut self,
-        id: S::EdgeId,
         weight: S::EdgeWeight,
 
-        source: S::NodeId,
-        target: S::NodeId,
-    ) -> Result<EdgeMut<S>, Error> {
-        if self.storage.contains_edge(id) {
-            let mut edge = self
-                .storage
-                .edge_mut(id)
-                .expect("inconsistent storage, edge must exist");
+        source: NodeId,
+        target: NodeId,
+    ) -> Result<Vec<EdgeId>, Error> {
+        let mut affected = vec![];
 
-            *edge.weight_mut() = weight;
-
-            Ok(edge)
-        } else {
-            self.storage
-                .insert_edge(id, weight, source, target)
-                .change_context(Error)
+        for mut edge in self.storage.edges_between_mut(source, target) {
+            *edge.weight_mut() = weight.clone();
+            affected.push(edge.id());
         }
+
+        if !affected.is_empty() {
+            return Ok(affected);
+        }
+
+        self.try_insert_edge(weight, source, target)
+            .map(|edge| vec![edge.id()])
     }
 
     /// Insert an edge, or update the weight of an existing edge, if it exists.
     ///
+    /// If multiple edges exist between the given nodes, all of them will be updated with the given
+    /// value.
+    ///
     /// This is the infallible version of [`Self::try_upsert_edge`], which will panic instead.
     ///
     /// # Panics
@@ -470,13 +403,71 @@
     /// [`Self::insert_edge`].
     pub fn upsert_edge(
         &mut self,
-        id: S::EdgeId,
         weight: S::EdgeWeight,
 
-        source: S::NodeId,
-        target: S::NodeId,
-    ) -> EdgeMut<S> {
-        self.try_upsert_edge(id, weight, source, target)
+        source: NodeId,
+        target: NodeId,
+    ) -> Vec<EdgeId> {
+        self.try_upsert_edge(weight, source, target)
             .expect("Constraint violation. Try using `try_upsert_edge` instead.")
     }
+
+    /// Insert an edge, or update the weight of an existing edge, if it exists.
+    ///
+    /// If multiple edges exist between the given nodes, all of them will invoke the given closure
+    /// with the edge.
+    ///
+    /// Unlike [`Self::try_upsert_edge_with`], this will not return the edge, and instead return a
+    /// list containing all affected edge identifiers.
+    ///
+    /// This is the fallible version of [`Self::upsert_edge_with`].
+    ///
+    /// # Errors
+    ///
+    /// The same errors as [`Self::try_insert_edge`] may occur.
+    pub fn try_upsert_edge_with(
+        &mut self,
+        mut on_update: impl FnMut(&mut EdgeMut<S>) -> S::EdgeWeight,
+        on_insert: impl FnOnce(EdgeId) -> S::EdgeWeight,
+
+        source: NodeId,
+        target: NodeId,
+    ) -> Result<Vec<EdgeId>, Error> {
+        let mut affected = vec![];
+
+        for mut edge in self.storage.edges_between_mut(source, target) {
+            *edge.weight_mut() = on_update(&mut edge);
+            affected.push(edge.id());
+        }
+
+        if !affected.is_empty() {
+            return Ok(affected);
+        }
+
+        self.try_insert_edge_with(on_insert, source, target)
+            .map(|edge| vec![edge.id()])
+    }
+
+    /// Insert an edge, or update the weight of an existing edge, if it exists.
+    ///
+    /// If multiple edges exist between the given nodes, all of them will invoke the given closure
+    /// with the edge.
+    ///
+    /// This is the infallible version of [`Self::try_upsert_edge_with`], which will panic instead.
+    ///
+    /// # Panics
+    ///
+    /// The same panics as [`Self::try_upsert_edge_with`] may occur, as well as the ones from
+    /// [`Self::insert_edge_with`].
+    pub fn upsert_edge_with(
+        &mut self,
+        on_update: impl FnMut(&mut EdgeMut<S>) -> S::EdgeWeight,
+        on_insert: impl FnOnce(EdgeId) -> S::EdgeWeight,
+
+        source: NodeId,
+        target: NodeId,
+    ) -> Vec<EdgeId> {
+        self.try_upsert_edge_with(on_update, on_insert, source, target)
+            .expect("Constraint violation. Try using `try_upsert_edge_with` instead.")
+    }
 }
diff --git a/crates/core/src/graph/mod.rs b/crates/core/src/graph/mod.rs
index 8937b61..3ddc677 100644
--- a/crates/core/src/graph/mod.rs
+++ b/crates/core/src/graph/mod.rs
@@ -3,12 +3,13 @@
 mod insert;
 mod resize;
 mod retain;
+mod reverse;
 
 use error_stack::Result;
 
 use crate::{
-    edge::{DetachedEdge, Edge, EdgeMut},
-    node::{DetachedNode, Node, NodeMut},
+    edge::{DetachedEdge, Edge, EdgeId, EdgeMut},
+    node::{DetachedNode, Node, NodeId, NodeMut},
     storage::GraphStorage,
 };
 
@@ -260,8 +261,8 @@
     /// If any of the nodes or edges are invalid, or any of the constraint checks of the underlying
     /// implementation fail, an error is returned.
     pub fn from_parts(
-        nodes: impl IntoIterator<Item = DetachedNode<S::NodeId, S::NodeWeight>>,
-        edges: impl IntoIterator<Item = DetachedEdge<S::EdgeId, S::NodeId, S::EdgeWeight>>,
+        nodes: impl IntoIterator<Item = DetachedNode<S::NodeWeight>>,
+        edges: impl IntoIterator<Item = DetachedEdge<S::EdgeWeight>>,
     ) -> Result<Self, S::Error> {
         Ok(Self {
             storage: S::from_parts(nodes, edges)?,
@@ -317,8 +318,8 @@
     pub fn into_parts(
         self,
     ) -> (
-        impl IntoIterator<Item = DetachedNode<S::NodeId, S::NodeWeight>>,
-        impl IntoIterator<Item = DetachedEdge<S::EdgeId, S::NodeId, S::EdgeWeight>>,
+        impl IntoIterator<Item = DetachedNode<S::NodeWeight>>,
+        impl IntoIterator<Item = DetachedEdge<S::EdgeWeight>>,
     ) {
         self.storage.into_parts()
     }
@@ -338,12 +339,7 @@
     // TODO: example
     pub fn convert<T>(self) -> Result<Graph<T>, T::Error>
     where
-        T: GraphStorage<
-                NodeId = S::NodeId,
-                NodeWeight = S::NodeWeight,
-                EdgeId = S::EdgeId,
-                EdgeWeight = S::EdgeWeight,
-            >,
+        T: GraphStorage<NodeWeight = S::NodeWeight, EdgeWeight = S::EdgeWeight>,
     {
         let (nodes, edges) = self.storage.into_parts();
 
@@ -472,7 +468,7 @@
     ///     None
     /// );
     /// ```
-    pub fn node(&self, id: S::NodeId) -> Option<Node<S>> {
+    pub fn node(&self, id: NodeId) -> Option<Node<S>> {
         self.storage.node(id)
     }
 
@@ -502,7 +498,7 @@
     ///
     /// assert!(graph.node_mut(&b).is_none());
     /// ```
-    pub fn node_mut(&mut self, id: S::NodeId) -> Option<NodeMut<S>> {
+    pub fn node_mut(&mut self, id: NodeId) -> Option<NodeMut<S>> {
         self.storage.node_mut(id)
     }
 
@@ -526,7 +522,7 @@
     /// assert!(graph.contains_node(&a));
     /// assert!(!graph.contains_node(&b));
     /// ```
-    pub fn contains_node(&self, id: S::NodeId) -> bool {
+    pub fn contains_node(&self, id: NodeId) -> bool {
         self.storage.contains_node(id)
     }
 
@@ -555,7 +551,7 @@
     /// assert_eq!(graph.remove_node(&a), None);
     /// assert_eq!(graph.remove_node(&b), None);
     /// ```
-    pub fn remove_node(&mut self, id: S::NodeId) -> Option<DetachedNode<S::NodeId, S::NodeWeight>> {
+    pub fn remove_node(&mut self, id: NodeId) -> Option<DetachedNode<S::NodeWeight>> {
         self.storage.remove_node(id)
     }
 
@@ -586,7 +582,7 @@
     /// );
     /// assert!(graph.edge(&bc).is_none());
     /// ```
-    pub fn edge(&self, id: S::EdgeId) -> Option<Edge<S>> {
+    pub fn edge(&self, id: EdgeId) -> Option<Edge<S>> {
         self.storage.edge(id)
     }
 
@@ -618,7 +614,7 @@
     /// );
     /// assert!(graph.edge_mut(&bc).is_none());
     /// ```
-    pub fn edge_mut(&mut self, id: S::EdgeId) -> Option<EdgeMut<S>> {
+    pub fn edge_mut(&mut self, id: EdgeId) -> Option<EdgeMut<S>> {
         self.storage.edge_mut(id)
     }
 
@@ -643,7 +639,7 @@
     /// assert!(graph.contains_edge(&ab));
     /// assert!(!graph.contains_edge(&bc));
     /// ```
-    pub fn contains_edge(&self, id: S::EdgeId) -> bool {
+    pub fn contains_edge(&self, id: EdgeId) -> bool {
         self.storage.contains_edge(id)
     }
 
@@ -678,10 +674,7 @@
     /// assert_eq!(graph.remove_edge(&ab), None);
     /// assert_eq!(graph.remove_edge(&bc), None);
     /// ```
-    pub fn remove_edge(
-        &mut self,
-        id: S::EdgeId,
-    ) -> Option<DetachedEdge<S::EdgeId, S::NodeId, S::EdgeWeight>> {
+    pub fn remove_edge(&mut self, id: EdgeId) -> Option<DetachedEdge<S::EdgeWeight>> {
         self.storage.remove_edge(id)
     }
 
@@ -690,7 +683,7 @@
     /// This is an alias for [`Self::neighbours`], as there's a spelling difference between the
     /// American and British English.
     #[inline]
-    pub fn neighbors(&self, id: S::NodeId) -> impl Iterator<Item = Node<'_, S>> {
+    pub fn neighbors(&self, id: NodeId) -> impl Iterator<Item = Node<'_, S>> {
         self.neighbours(id)
     }
 
@@ -736,7 +729,7 @@
     ///     [a, c].into_iter().collect::<HashSet<_>>()
     /// );
     /// ```
-    pub fn neighbours(&self, id: S::NodeId) -> impl Iterator<Item = Node<'_, S>> {
+    pub fn neighbours(&self, id: NodeId) -> impl Iterator<Item = Node<'_, S>> {
         self.storage.node_neighbours(id)
     }
 
@@ -745,7 +738,7 @@
     /// This is an alias for [`Self::neighbours_mut`], as there's a spelling difference between
     /// American and British English.
     #[inline]
-    pub fn neighbors_mut(&mut self, id: S::NodeId) -> impl Iterator<Item = NodeMut<'_, S>> {
+    pub fn neighbors_mut(&mut self, id: NodeId) -> impl Iterator<Item = NodeMut<'_, S>> {
         self.neighbours_mut(id)
     }
 
@@ -793,7 +786,7 @@
     ///     Some((d, 3))
     /// );
     /// ```
-    pub fn neighbours_mut(&mut self, id: S::NodeId) -> impl Iterator<Item = NodeMut<'_, S>> {
+    pub fn neighbours_mut(&mut self, id: NodeId) -> impl Iterator<Item = NodeMut<'_, S>> {
         self.storage.node_neighbours_mut(id)
     }
 
@@ -828,7 +821,7 @@
     ///     [ab, ca, aa].into_iter().collect::<HashSet<_>>()
     /// );
     /// ```
-    pub fn connections(&self, id: S::NodeId) -> impl Iterator<Item = Edge<'_, S>> {
+    pub fn connections(&self, id: NodeId) -> impl Iterator<Item = Edge<'_, S>> {
         self.storage.node_connections(id)
     }
 
@@ -874,7 +867,7 @@
     ///     Some((bc, u8::MAX - 1))
     /// );
     /// ```
-    pub fn connections_mut(&mut self, id: S::NodeId) -> impl Iterator<Item = EdgeMut<'_, S>> {
+    pub fn connections_mut(&mut self, id: NodeId) -> impl Iterator<Item = EdgeMut<'_, S>> {
         self.storage.node_connections_mut(id)
     }
 
@@ -905,7 +898,7 @@
     /// assert_eq!(graph.degree(&c), 2);
     /// assert_eq!(graph.degree(&d), 0);
     /// ```
-    pub fn degree(&self, id: S::NodeId) -> usize {
+    pub fn degree(&self, id: NodeId) -> usize {
         self.storage.node_degree(id)
     }
 
@@ -939,7 +932,7 @@
     ///     [ab, ba].into_iter().collect::<HashSet<_>>()
     /// );
     /// ```
-    pub fn edges_between(&self, u: S::NodeId, v: S::NodeId) -> impl Iterator<Item = Edge<'_, S>> {
+    pub fn edges_between(&self, u: NodeId, v: NodeId) -> impl Iterator<Item = Edge<'_, S>> {
         self.storage.edges_between(u, v)
     }
 
@@ -979,8 +972,8 @@
     /// ```
     pub fn edges_between_mut(
         &mut self,
-        u: S::NodeId,
-        v: S::NodeId,
+        u: NodeId,
+        v: NodeId,
     ) -> impl Iterator<Item = EdgeMut<'_, S>> {
         self.storage.edges_between_mut(u, v)
     }
diff --git a/crates/core/src/graph/reverse.rs b/crates/core/src/graph/reverse.rs
new file mode 100644
index 0000000..a922564
--- /dev/null
+++ b/crates/core/src/graph/reverse.rs
@@ -0,0 +1,30 @@
+use crate::{storage::reverse::ReverseGraphStorage, Edge, EdgeMut, Graph, Node, NodeMut};
+
+impl<S> Graph<S>
+where
+    S: ReverseGraphStorage,
+{
+    pub fn contains_node_key(&self, key: &S::NodeKey) -> bool {
+        self.storage.contains_node_key(key)
+    }
+
+    pub fn node_by_key(&self, key: &S::NodeKey) -> Option<Node<'_, S>> {
+        self.storage.node_by_key(key)
+    }
+
+    pub fn node_by_key_mut(&mut self, key: &S::NodeKey) -> Option<NodeMut<'_, S>> {
+        self.storage.node_by_key_mut(key)
+    }
+
+    pub fn contains_edge_key(&self, key: &S::EdgeKey) -> bool {
+        self.storage.contains_edge_key(key)
+    }
+
+    pub fn edge_by_key(&self, key: &S::EdgeKey) -> Option<Edge<'_, S>> {
+        self.storage.edge_by_key(key)
+    }
+
+    pub fn edge_by_key_mut(&mut self, key: &S::EdgeKey) -> Option<EdgeMut<'_, S>> {
+        self.storage.edge_by_key_mut(key)
+    }
+}
diff --git a/crates/core/src/id/associative.rs b/crates/core/src/id/associative.rs
deleted file mode 100644
index ed4a95b..0000000
--- a/crates/core/src/id/associative.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use crate::{GraphId, GraphStorage};
-
-// TODO: Entry API
-
-pub trait AttributeMapper<K, V> {
-    type Iter<'a>: Iterator<Item = (K, &'a V)>
-    where
-        V: 'a,
-        Self: 'a;
-
-    fn get(&self, id: K) -> Option<&V>;
-    fn get_mut(&mut self, id: K) -> Option<&mut V>;
-    fn index(&self, id: K) -> &V {
-        self.get(id).expect("item")
-    }
-    fn index_mut(&mut self, id: K) -> &mut V {
-        self.get_mut(id).expect("item")
-    }
-
-    fn set(&mut self, id: K, value: V) -> Option<V>;
-    fn remove(&mut self, id: K) -> Option<V>;
-
-    fn iter(&self) -> Self::Iter<'_>;
-}
-
-pub trait BooleanMapper<K> {
-    fn get(&self, id: K) -> Option<bool>;
-    #[inline]
-    fn index(&self, id: K) -> bool {
-        self.get(id).unwrap_or(false)
-    }
-
-    fn set(&mut self, id: K, flag: bool) -> Option<bool>;
-}
-
-pub trait AssociativeGraphId<S>: GraphId + Sized
-where
-    S: GraphStorage,
-{
-    type AttributeMapper<'a, V>: AttributeMapper<Self, V>
-    where
-        S: 'a;
-
-    type BooleanMapper<'a>: BooleanMapper<Self>
-    where
-        S: 'a;
-
-    fn attribute_mapper<V>(storage: &S) -> Self::AttributeMapper<'_, V>;
-
-    fn boolean_mapper(storage: &S) -> Self::BooleanMapper<'_>;
-}
diff --git a/crates/core/src/id/mod.rs b/crates/core/src/id/mod.rs
deleted file mode 100644
index 3cb1299..0000000
--- a/crates/core/src/id/mod.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-//! Module for identifiers.
-//!
-//! Identifiers are used to _identify_ edges and nodes.
-//!
-//! Primarily this module defines one type: [`GraphId`], which should not be implemented alone, but
-//! instead be implemented alongside [`ManagedGraphId`] and [`ArbitraryGraphId`], which are mutually
-//! exclusive and define if a node or edge id will be automatically assigned by the graph (are
-//! managed) and a user has no control over their value or are arbitrary, allowing the user to use
-//! _any_ value.
-mod associative;
-
-mod linear;
-
-use core::fmt::Debug;
-
-pub use self::{
-    associative::{AssociativeGraphId, AttributeMapper, BooleanMapper},
-    linear::{IndexMapper, LinearGraphId},
-};
-use crate::attributes::NoValue;
-
-// The `PartialEq` bound is required for the default implementation, we could in theory remove it,
-// but would need to remove _many_ default implementations that rely on it.
-// Another possibility would've been to use `Hash` for `GraphId`, but `ArbitaryGraphId` needs to be
-// a wrapper type anyway, so it could just require `Hash` for the inner type, and then implement
-// `PartialEq` based on that.
-/// A unique identifier for a node or edge.
-///
-/// This trait is implemented for all types that are used as node or edge identifiers in the graph.
-/// A type should never only implement this trait, but also [`ManagedGraphId`] or
-/// [`ArbitraryGraphId`].
-pub trait GraphId: Debug + Copy + Clone + PartialEq {
-    /// The type of value used to index attributes.
-    ///
-    /// Used to differentiate between [`ManagedGraphId`] and [`ArbitraryGraphId`] and to allow for
-    /// inference on weights via the [`Attributes`] type.
-    ///
-    /// There are essentially two valid values for this type: [`NoValue`] and `Self`.
-    ///
-    /// [`Attributes`]: crate::attributes::Attributes
-    type AttributeIndex;
-}
-
-/// A unique identifier for a node or edge that is managed by the graph.
-///
-/// Marker trait to indicate that the graph manages the identifier of the node or edge, and cannot
-/// be specified by the user itself.
-///
-/// This is analogous to an index in a `Vec`.
-pub trait ManagedGraphId: GraphId<AttributeIndex = NoValue> {}
-
-/// A unique identifier for a node or edge that is not managed by the graph.
-///
-/// Marker trait to indicate that the graph does not manage the identifier of the node or edge, and
-/// must be specified by the user itself.
-///
-/// This is analogous to a key in a `HashMap`.
-pub trait ArbitraryGraphId: GraphId<AttributeIndex = Self> {}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index ff52808..1d99b02 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -120,13 +120,11 @@
 #[cfg(feature = "alloc")]
 extern crate alloc;
 
-pub mod attributes;
 #[deprecated(since = "0.1.0")]
 pub mod deprecated;
 pub mod edge;
 mod error;
 pub(crate) mod graph;
-pub mod id;
 pub mod node;
 pub mod storage;
 
@@ -134,7 +132,6 @@
     edge::{DetachedEdge, Edge, EdgeMut, GraphDirectionality},
     error::Error,
     graph::Graph,
-    id::{ArbitraryGraphId, GraphId, ManagedGraphId},
     node::{DetachedNode, Node, NodeMut},
     storage::{DirectedGraphStorage, GraphStorage},
 };
diff --git a/crates/core/src/node/compat.rs b/crates/core/src/node/compat.rs
index 66759cc..487c97c 100644
--- a/crates/core/src/node/compat.rs
+++ b/crates/core/src/node/compat.rs
@@ -1,18 +1,21 @@
 //! Compatability implementations for deprecated graph traits.
 #![allow(deprecated)]
 
-use crate::{deprecated::visit::NodeRef, node::Node, storage::GraphStorage};
+use crate::{
+    deprecated::visit::NodeRef,
+    node::{Node, NodeId},
+    storage::GraphStorage,
+};
 
 impl<S> NodeRef for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Clone,
 {
-    type NodeId = S::NodeId;
+    type NodeId = NodeId;
     type Weight = S::NodeWeight;
 
     fn id(&self) -> Self::NodeId {
-        self.id.clone()
+        self.id
     }
 
     fn weight(&self) -> &Self::Weight {
diff --git a/crates/core/src/node/mod.rs b/crates/core/src/node/mod.rs
index 716f6f3..3e1e845 100644
--- a/crates/core/src/node/mod.rs
+++ b/crates/core/src/node/mod.rs
@@ -17,7 +17,7 @@
 mod compat;
 
 use core::{
-    fmt::{Debug, Formatter},
+    fmt::{Debug, Display, Formatter},
     hash::Hash,
 };
 
@@ -26,6 +26,77 @@
     storage::{DirectedGraphStorage, GraphStorage},
 };
 
+/// ID of a node in a graph.
+///
+/// This is guaranteed to be unique within the graph, library authors and library consumers **must**
+/// treat this as an opaque value akin to [`TypeId`].
+///
+/// The layout of the type is semver stable, but not part of the public API.
+///
+/// [`GraphStorage`] implementations may uphold additional invariants on the inner value and
+/// code outside of the [`GraphStorage`] should **never** construct a [`NodeId`] directly.
+///
+/// Accessing a [`GraphStorage`] implementation with a [`NodeId`] not returned by an instance itself
+/// is considered undefined behavior.
+///
+/// [`TypeId`]: core::any::TypeId
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct NodeId(usize);
+
+impl Display for NodeId {
+    // we could also utilize a VTable here instead, that would allow for custom formatting
+    // but that would be an additional pointer added to the type that must be carried around
+    // that's about ~8 bytes on 64-bit systems
+    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+        writeln!(f, "NodeId({})", self.0)
+    }
+}
+
+// TODO: find a better way to gate these functions
+impl NodeId {
+    /// Creates a new [`NodeId`].
+    ///
+    /// # Note
+    ///
+    /// Using this outside of the [`GraphStorage`] implementation is considered undefined behavior.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use petgraph_core::node::NodeId;
+    ///
+    /// let id = NodeId::new(0);
+    /// ```
+    // Hidden so that non-GraphStorage implementors are not tempted to use this.
+    #[doc(hidden)]
+    #[must_use]
+    pub const fn new(id: usize) -> Self {
+        Self(id)
+    }
+
+    /// Returns the inner value of the [`NodeId`].
+    ///
+    /// # Note
+    ///
+    /// Using this outside of the [`GraphStorage`] implementation is considered undefined behavior.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use petgraph_core::node::NodeId;
+    ///
+    /// let id = NodeId::new(0);
+    ///
+    /// assert_eq!(id.into_inner(), 0);
+    /// ```
+    // Hidden so that non-GraphStorage implementors are not tempted to use this.
+    #[doc(hidden)]
+    #[must_use]
+    pub const fn into_inner(self) -> usize {
+        self.0
+    }
+}
+
 /// Active node in a graph.
 ///
 /// Node that is part of a graph.
@@ -53,14 +124,13 @@
 {
     storage: &'a S,
 
-    id: S::NodeId,
+    id: NodeId,
     weight: &'a S::NodeWeight,
 }
 
 impl<S> PartialEq for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: PartialEq,
     S::NodeWeight: PartialEq,
 {
     fn eq(&self, other: &Node<'_, S>) -> bool {
@@ -71,7 +141,6 @@
 impl<S> Eq for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Eq,
     S::NodeWeight: Eq,
 {
 }
@@ -79,7 +148,6 @@
 impl<S> PartialOrd for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: PartialOrd,
     S::NodeWeight: PartialOrd,
 {
     fn partial_cmp(&self, other: &Node<'_, S>) -> Option<core::cmp::Ordering> {
@@ -90,7 +158,6 @@
 impl<S> Ord for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Ord,
     S::NodeWeight: Ord,
 {
     fn cmp(&self, other: &Node<'_, S>) -> core::cmp::Ordering {
@@ -101,7 +168,6 @@
 impl<S> Hash for Node<'_, S>
 where
     S: GraphStorage,
-    S::NodeId: Hash,
     S::NodeWeight: Hash,
 {
     fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
@@ -168,7 +234,7 @@
     /// ```
     ///
     /// [`Graph::node`]: crate::graph::Graph::node
-    pub const fn new(storage: &'a S, id: S::NodeId, weight: &'a S::NodeWeight) -> Self {
+    pub const fn new(storage: &'a S, id: NodeId, weight: &'a S::NodeWeight) -> Self {
         Self {
             storage,
             id,
@@ -197,7 +263,7 @@
     /// assert_eq!(node.id(), &a);
     /// ```
     #[must_use]
-    pub const fn id(&self) -> S::NodeId {
+    pub const fn id(&self) -> NodeId {
         self.id
     }
 
@@ -318,6 +384,28 @@
     pub fn degree(&self) -> usize {
         self.storage.node_degree(self.id)
     }
+
+    /// Change the underlying storage of the node.
+    ///
+    /// Should only be used when layering multiple [`GraphStorage`] implementations on top of each
+    /// other.
+    ///
+    /// # Note
+    ///
+    /// This should not lead to any undefined behaviour, but might have unintended consequences if
+    /// the storage does not recognize the inner id as valid.
+    /// You should only use this if you know what you are doing.
+    #[must_use]
+    pub const fn change_storage_unchecked<T>(self, storage: &'a T) -> Node<'a, T>
+    where
+        T: GraphStorage<NodeWeight = S::NodeWeight>,
+    {
+        Node {
+            storage,
+            id: self.id,
+            weight: self.weight,
+        }
+    }
 }
 
 impl<'a, S> Node<'a, S>
@@ -442,7 +530,7 @@
     ///
     /// [`Graph::from_parts`]: crate::graph::Graph::from_parts
     #[must_use]
-    pub fn detach(self) -> DetachedNode<S::NodeId, S::NodeWeight> {
+    pub fn detach(self) -> DetachedNode<S::NodeWeight> {
         DetachedNode::new(self.id, self.weight.clone())
     }
 }
@@ -474,7 +562,7 @@
 where
     S: GraphStorage,
 {
-    id: S::NodeId,
+    id: NodeId,
 
     weight: &'a mut S::NodeWeight,
 }
@@ -498,7 +586,7 @@
     ///
     /// [`Graph::node_mut`]: crate::graph::Graph::node_mut
     /// [`Graph::insert_node`]: crate::graph::Graph::insert_node
-    pub fn new(id: S::NodeId, weight: &'a mut S::NodeWeight) -> Self {
+    pub fn new(id: NodeId, weight: &'a mut S::NodeWeight) -> Self {
         Self { id, weight }
     }
 
@@ -523,7 +611,7 @@
     /// assert_eq!(node.id(), &a);
     /// ```
     #[must_use]
-    pub const fn id(&self) -> S::NodeId {
+    pub const fn id(&self) -> NodeId {
         self.id
     }
 
@@ -569,6 +657,27 @@
     pub fn weight_mut(&mut self) -> &mut S::NodeWeight {
         self.weight
     }
+
+    /// Change the underlying storage of the node.
+    ///
+    /// Should only be used when layering multiple [`GraphStorage`] implementations on top of each
+    /// other.
+    ///
+    /// # Note
+    ///
+    /// This should not lead to any undefined behaviour, but might have unintended consequences if
+    /// the storage does not recognize the inner id as valid.
+    /// You should only use this if you know what you are doing.
+    #[must_use]
+    pub fn change_storage_unchecked<T>(self) -> NodeMut<'a, T>
+    where
+        T: GraphStorage<NodeWeight = S::NodeWeight>,
+    {
+        NodeMut {
+            id: self.id,
+            weight: self.weight,
+        }
+    }
 }
 
 impl<S> NodeMut<'_, S>
@@ -607,7 +716,7 @@
     ///
     /// [`Graph::from_parts`]: crate::graph::Graph::from_parts
     #[must_use]
-    pub fn detach(&self) -> DetachedNode<S::NodeId, S::NodeWeight> {
+    pub fn detach(&self) -> DetachedNode<S::NodeWeight> {
         DetachedNode::new(self.id, self.weight.clone())
     }
 }
@@ -640,15 +749,15 @@
 /// [`Graph::into_parts`]: crate::graph::Graph::into_parts
 /// [`Graph::from_parts`]: crate::graph::Graph::from_parts
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct DetachedNode<N, W> {
+pub struct DetachedNode<W> {
     /// The unique id of the node.
-    pub id: N,
+    pub id: NodeId,
 
     /// The weight of the node.
     pub weight: W,
 }
 
-impl<N, W> DetachedNode<N, W> {
+impl<W> DetachedNode<W> {
     /// Creates a new detached node.
     ///
     /// # Example
@@ -658,7 +767,7 @@
     ///
     /// let node = DetachedNode::new(0, "A");
     /// ```
-    pub const fn new(id: N, weight: W) -> Self {
+    pub const fn new(id: NodeId, weight: W) -> Self {
         Self { id, weight }
     }
 }
diff --git a/crates/core/src/storage/auxiliary.rs b/crates/core/src/storage/auxiliary.rs
new file mode 100644
index 0000000..596aa67
--- /dev/null
+++ b/crates/core/src/storage/auxiliary.rs
@@ -0,0 +1,122 @@
+//! Auxiliary storage for graphs.
+//!
+//! This module provides traits for auxiliary storage for graphs, which can be used to associate
+//! arbitrary additional data with nodes and edges.
+use crate::{edge::EdgeId, node::NodeId};
+
+/// Hints about frequency.
+///
+/// These hints are used to optimize the performance of secondary storage, specifically these are
+/// used to tell how frequently data is expected to be accessed.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum FrequencyHint {
+    /// Items are accessed frequently.
+    #[default]
+    Frequent,
+    /// Items are accessed infrequently.
+    Infrequent,
+}
+
+/// Hints about performance.
+///
+/// These hints are used to optimize the performance of secondary storage, specifically these are
+/// used to tell how frequently data is expected to be accessed.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct PerformanceHint {
+    /// Hints about the frequency of reads.
+    pub read: FrequencyHint,
+    /// Hints about the frequency of writes.
+    pub write: FrequencyHint,
+}
+
+/// Hints about occupancy.
+///
+/// These hints are used to optimize the performance of secondary storage, specifically these are
+/// used to tell how much space is expected to be filled with data.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum OccupancyHint {
+    /// Storage is expected to be densely populated.
+    #[default]
+    Dense,
+    /// Storage is expected to be sparsely populated.
+    Sparse,
+}
+
+/// Hints for secondary storage.
+///
+/// These hints are used to optimize the performance of secondary storage.
+///
+/// These hints are not guaranteed to be respected, and are used as a best-effort heuristic.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Hints {
+    /// Hints about performance.
+    pub performance: PerformanceHint,
+    /// Hints about occupancy.
+    pub occupancy: OccupancyHint,
+}
+
+/// Secondary storage for a graph.
+///
+/// This trait is used to provide secondary storage for a graph to associate arbitrary additional
+/// data with nodes and edges.
+///
+/// If you want to only store boolean values for nodes and edges, you can use the
+/// [`BooleanGraphStorage`] trait instead, which usually has a more efficient implementation.
+pub trait SecondaryGraphStorage<K, V> {
+    type Iter<'a>: Iterator<Item = (K, &'a V)>
+    where
+        V: 'a,
+        Self: 'a;
+
+    fn get(&self, id: K) -> Option<&V>;
+    fn get_mut(&mut self, id: K) -> Option<&mut V>;
+
+    fn set(&mut self, id: K, value: V) -> Option<V>;
+    fn remove(&mut self, id: K) -> Option<V>;
+
+    fn iter(&self) -> Self::Iter<'_>;
+}
+
+/// Secondary storage for a graph.
+///
+/// This trait is used to provide secondary storage for a graph to associate boolean values with
+/// nodes and edges.
+pub trait BooleanGraphStorage<K> {
+    fn get(&self, id: K) -> Option<bool>;
+
+    fn set(&mut self, id: K, flag: bool) -> Option<bool>;
+
+    fn fill(&mut self, flag: bool);
+}
+
+/// Auxiliary storage for a graph.
+///
+/// Provides secondary storage for a graph to associate arbitrary additional data with nodes and
+/// edges, as well as boolean values with nodes and edges.
+///
+/// For boolean values prefer [`Self::boolean_edge_storage`] and [`Self::boolean_node_storage`],
+/// as they usually have a more efficient implementation.
+pub trait AuxiliaryGraphStorage {
+    type BooleanEdgeStorage<'graph>: BooleanGraphStorage<EdgeId>
+    where
+        Self: 'graph;
+
+    type BooleanNodeStorage<'graph>: BooleanGraphStorage<NodeId>
+    where
+        Self: 'graph;
+
+    type SecondaryEdgeStorage<'graph, V>: SecondaryGraphStorage<EdgeId, V>
+    where
+        Self: 'graph;
+
+    type SecondaryNodeStorage<'graph, V>: SecondaryGraphStorage<NodeId, V>
+    where
+        Self: 'graph;
+
+    fn secondary_node_storage<V>(&self, hints: Hints) -> Self::SecondaryNodeStorage<'_, V>;
+    fn secondary_edge_storage<V>(&self, hints: Hints) -> Self::SecondaryEdgeStorage<'_, V>;
+
+    fn boolean_node_storage(&self, hints: Hints) -> Self::BooleanNodeStorage<'_>;
+
+    fn boolean_edge_storage(&self, hints: Hints) -> Self::BooleanEdgeStorage<'_>;
+}
diff --git a/crates/core/src/storage/directed.rs b/crates/core/src/storage/directed.rs
index 8837fb6..0a7387f 100644
--- a/crates/core/src/storage/directed.rs
+++ b/crates/core/src/storage/directed.rs
@@ -1,6 +1,6 @@
 use crate::{
     edge::{Direction, Edge, EdgeMut},
-    node::{Node, NodeMut},
+    node::{Node, NodeId, NodeMut},
     storage::GraphStorage,
 };
 
@@ -75,8 +75,8 @@
     /// Most implementations should be able to provide a more efficient implementation.
     fn directed_edges_between(
         &self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = Edge<'_, Self>> {
         self.node_directed_connections(source, Direction::Outgoing)
             .filter(move |edge| edge.target_id() == target)
@@ -130,8 +130,8 @@
     /// Most implementations should be able to provide a more efficient implementation.
     fn directed_edges_between_mut(
         &mut self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = EdgeMut<'_, Self>> {
         self.node_directed_connections_mut(source, Direction::Outgoing)
             .filter(move |edge| edge.target_id() == target)
@@ -191,7 +191,7 @@
     /// ```
     fn node_directed_connections(
         &self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Edge<'_, Self>>;
 
@@ -245,7 +245,7 @@
     /// ```
     fn node_directed_connections_mut(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = EdgeMut<'_, Self>>;
 
@@ -280,7 +280,7 @@
     /// assert_eq!(storage.node_directed_degree(&a, Direction::Outgoing), 1);
     /// assert_eq!(storage.node_directed_degree(&a, Direction::Incoming), 1);
     /// ```
-    fn node_directed_degree(&self, id: Self::NodeId, direction: Direction) -> usize {
+    fn node_directed_degree(&self, id: NodeId, direction: Direction) -> usize {
         self.node_directed_connections(id, direction).count()
     }
 
@@ -345,7 +345,7 @@
     /// and must uphold the contract that the returned iterator does not contain duplicates.
     fn node_directed_neighbours(
         &self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Node<'_, Self>> {
         self.node_directed_connections(id, direction)
@@ -414,7 +414,7 @@
     // I'd love to provide a default implementation for this, but I just can't get it to work.
     fn node_directed_neighbours_mut(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = NodeMut<'_, Self>>;
 }
diff --git a/crates/core/src/storage/mod.rs b/crates/core/src/storage/mod.rs
index c7ce1cc..0d1910d 100644
--- a/crates/core/src/storage/mod.rs
+++ b/crates/core/src/storage/mod.rs
@@ -13,6 +13,9 @@
 //!   to an undirected graph.
 //! - [`DirectedGraphStorage`]: A trait for directed graph storage implementations.
 //! - [`RetainableGraphStorage`]: A trait for retainable graph storage implementations.
+//! - [`AuxiliaryGraphStorage`]: A trait to access storage for arbitrary additional data.
+//! - [`SequentialGraphStorage`]: A trait for graph storage implementations that allow the mapping
+//!   of their internal indices to a set of linear indices.
 //!
 //! [`GraphStorage`] proposes that [`DirectedGraphStorage`] is simply a specialization of an
 //! undirected graph, meaning that the supertrait of [`DirectedGraphStorage`] is also
@@ -21,20 +24,27 @@
 //!
 //! # Implementation Notes
 //!
-//! [`RetainableGraphStorage`] is subject to removal during the alpha period.
+//! * [`RetainableGraphStorage`] is subject to removal during the alpha period.
+//! * [`SequentialGraphStorage`] is subject to removal or rename during the alpha period.
+//! * [`AuxiliaryGraphStorage`] is subject to removal or rename during the alpha period.
 //!
 //! [`Graph`]: crate::graph::Graph
 mod directed;
 
+pub mod auxiliary;
 mod retain;
+pub mod reverse;
+pub mod sequential;
 
 use error_stack::{Context, Result};
 
-pub use self::{directed::DirectedGraphStorage, retain::RetainableGraphStorage};
+pub use self::{
+    auxiliary::AuxiliaryGraphStorage, directed::DirectedGraphStorage,
+    retain::RetainableGraphStorage, sequential::SequentialGraphStorage,
+};
 use crate::{
-    edge::{DetachedEdge, Edge, EdgeMut},
-    id::GraphId,
-    node::{DetachedNode, Node, NodeMut},
+    edge::{DetachedEdge, Edge, EdgeId, EdgeMut},
+    node::{DetachedNode, Node, NodeId, NodeMut},
 };
 
 /// A trait for graph storage implementations.
@@ -115,39 +125,9 @@
 /// [`Graph::new`]: crate::graph::Graph::new
 /// [`Graph::new_in`]: crate::graph::Graph::new_in
 /// [`Graph::with_capacity`]: crate::graph::Graph::with_capacity
-pub trait GraphStorage: Sized {
-    /// The unique identifier for an edge.
-    ///
-    /// This is used to identify edges in the graph.
-    /// The equivalent in a `HashMap` would be the key.
-    /// In contrast to a `HashMap` (or similar data structures) the trait does not enforce any
-    /// additional constraints,
-    /// implementations are free to choose to limit identifiers to a certain subset if required, or
-    /// choose a concrete type.
-    ///
-    /// Fundamentally, while [`Self::EdgeId`] must be of type [`GraphId`], the chosen
-    /// [`Self::EdgeId`] of an implementation should either implement [`ManagedGraphId`] or
-    /// [`ArbitraryGraphId`], which work as marker traits.
-    ///
-    /// [`ManagedGraphId`] indicates that the implementation manages the identifiers, subsequently,
-    /// users are not allowed to create identifiers themselves.
-    /// [`ArbitraryGraphId`] indicates that the implementation does not manage the identifiers, and
-    /// users are allowed to create identifiers themselves.
-    /// This is reflected in the API through the [`Attributes`] type, which needs to be supplied
-    /// when creating edges, if the implementation does not manage the identifiers, [`Attributes`]
-    /// will allow users to specify the identifier and weight of the edge, while if the
-    /// implementation manages the identifiers, [`Attributes`] will only allow users to specify the
-    /// weight of the edge.
-    ///
-    /// [`Attributes`]: crate::attributes::Attributes
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    /// [`ArbitraryGraphId`]: crate::id::ArbitraryGraphId
-    type EdgeId: GraphId;
-
+pub trait GraphStorage: SequentialGraphStorage + AuxiliaryGraphStorage + Sized {
     /// The weight of an edge.
     ///
-    /// This works in tandem with [`Self::EdgeId`], and is used to store the weight of an edge.
-    /// The equivalent in a `HashMap` would be the value.
     /// No constraints are enforced on this type (except that it needs to be `Sized`), but
     /// implementations _may_ choose to enforce additional constraints, or limit the type to a
     /// specific concrete type.
@@ -171,38 +151,8 @@
     /// [`Report`]: error_stack::Report
     type Error: Context;
 
-    /// The unique identifier for a node.
-    ///
-    /// This is used to identify nodes in the graph.
-    /// The equivalent in a `HashMap` would be the key.
-    /// In contrast to a `HashMap` (or similar data structures) the trait does not enforce any
-    /// additional constraints,
-    /// implementations are free to choose to limit identifiers to a certain subset if required, or
-    /// choose a concrete type.
-    ///
-    /// Fundamentally, while [`Self::NodeId`] must be of type [`GraphId`], the chosen
-    /// [`Self::NodeId`] of an implementation should either implement [`ManagedGraphId`] or
-    /// [`ArbitraryGraphId`], which work as marker traits.
-    ///
-    /// [`ManagedGraphId`] indicates that the implementation manages the identifiers, subsequently,
-    /// users are not allowed to create identifiers themselves.
-    /// [`ArbitraryGraphId`] indicates that the implementation does not manage the identifiers, and
-    /// users are allowed to create identifiers themselves.
-    /// This is reflected in the API through the [`Attributes`] type, which needs to be supplied
-    /// when creating a node, if the implementation does not manage the identifiers, [`Attributes`]
-    /// will allow users to specify the identifier and weight of the node, while if the
-    /// implementation manages the identifiers, [`Attributes`] will only allow users to specify the
-    /// weight of the node.
-    ///
-    /// [`Attributes`]: crate::attributes::Attributes
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    /// [`ArbitraryGraphId`]: crate::id::ArbitraryGraphId
-    type NodeId: GraphId;
-
     /// The weight of a node.
     ///
-    /// This works in tandem with [`Self::NodeId`], and is used to store the weight of a node.
-    /// The equivalent in a `HashMap` would be the value.
     /// No constraints are enforced on this type (except that it needs to be `Sized`), but
     /// implementations _may_ choose to enforce additional constraints, or limit the type to a
     /// specific concrete type.
@@ -243,13 +193,9 @@
     /// This is the reverse operation of [`Self::into_parts`], which converts the current graph
     /// storage into an iterable of nodes and edges.
     ///
-    /// The ordering of the nodes and edges in the resulting graph storage is not preserved, if that
-    /// is the case in a storage implementation it should be considered an implementation
-    /// detail and not be relied upon.
-    /// The same applies to identifiers of nodes and edges, which may be changed during
-    /// construction.
-    /// The only properties that can be relied upon is that all nodes and edges will be present,
-    /// their weights will be the same, and that the graph will be structurally identical.
+    /// This process is lossy, neither the node ids or the edge ids are guaranteed to be preserved.
+    /// This function only guarantees that the weights of the nodes and edges are preserved as well
+    /// as the structure of the graph.
     ///
     /// # Example
     ///
@@ -294,10 +240,12 @@
     ///
     /// Implementations may choose to override this default implementation, but should try to also
     /// be fail-slow.
+    // TODO: additionally should return a mapping!
     fn from_parts(
-        nodes: impl IntoIterator<Item = DetachedNode<Self::NodeId, Self::NodeWeight>>,
-        edges: impl IntoIterator<Item = DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>>,
+        nodes: impl IntoIterator<Item = DetachedNode<Self::NodeWeight>>,
+        edges: impl IntoIterator<Item = DetachedEdge<Self::EdgeWeight>>,
     ) -> Result<Self, Self::Error> {
+        // TODO: rework this!
         let nodes = nodes.into_iter();
         let edges = edges.into_iter();
 
@@ -386,8 +334,8 @@
     fn into_parts(
         self,
     ) -> (
-        impl Iterator<Item = DetachedNode<Self::NodeId, Self::NodeWeight>>,
-        impl Iterator<Item = DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>>,
+        impl Iterator<Item = DetachedNode<Self::NodeWeight>>,
+        impl Iterator<Item = DetachedEdge<Self::EdgeWeight>>,
     );
 
     /// Returns the number of nodes in the graph.
@@ -465,9 +413,6 @@
     /// and is instead used by the [`Graph`] type to generate a new identifier that is then used
     /// during [`Graph::insert_node`].
     ///
-    /// This function is only of interest for implementations that manage the identifiers of nodes
-    /// (using the [`ManagedGraphId`] marker trait).
-    ///
     /// # Example
     ///
     /// ```
@@ -500,18 +445,7 @@
     ///
     /// The implementation of this function must also be fast, as it is called every time a new node
     /// is inserted and must be pure, meaning that it must not have any side-effects.
-    ///
-    /// If the [`Self::NodeId`] is a [`ManagedGraphId`], the implementation of this function must
-    /// return [`Self::NodeId`] and must not take `attribute` into account (in fact it can't, as the
-    /// value is always [`NoValue`]). Should the [`ArbitraryGraphId`] marker trait be implemented,
-    /// this function should effectively be a no-op and given `attribute`.
-    ///
-    /// [`Graph`]: crate::graph::Graph
-    /// [`Graph::insert_node`]: crate::graph::Graph::insert_node
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    /// [`ArbitraryGraphId`]: crate::id::ArbitraryGraphId
-    /// [`NoValue`]: crate::attributes::NoValue
-    fn next_node_id(&self, attribute: <Self::NodeId as GraphId>::AttributeIndex) -> Self::NodeId;
+    fn next_node_id(&self) -> NodeId;
 
     /// Inserts a new node into the graph.
     ///
@@ -537,7 +471,7 @@
     /// constraints (depending on the implementation) are violated.
     fn insert_node(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
 
         weight: Self::NodeWeight,
     ) -> Result<NodeMut<Self>, Self::Error>;
@@ -548,9 +482,6 @@
     /// and is instead used by the [`Graph`] type to generate a new identifier that is then used
     /// during [`Graph::insert_edge`].
     ///
-    /// This function is only of interest for implementations that manage the identifiers of edges
-    /// (using the [`ManagedGraphId`] marker trait).
-    ///
     /// # Example
     ///
     /// ```
@@ -573,8 +504,7 @@
     ///
     /// [`Graph`]: crate::graph::Graph
     /// [`Graph::insert_edge`]: crate::graph::Graph::insert_edge
-    /// [`ManagedGraphId`]: crate::id::ManagedGraphId
-    fn next_edge_id(&self, attribute: <Self::EdgeId as GraphId>::AttributeIndex) -> Self::EdgeId;
+    fn next_edge_id(&self) -> EdgeId;
 
     /// Inserts a new edge into the graph.
     ///
@@ -611,11 +541,11 @@
     /// but some implementations may choose to allow parallel edges.
     fn insert_edge(
         &mut self,
-        id: Self::EdgeId,
+        id: EdgeId,
         weight: Self::EdgeWeight,
 
-        u: Self::NodeId,
-        v: Self::NodeId,
+        u: NodeId,
+        v: NodeId,
     ) -> Result<EdgeMut<Self>, Self::Error>;
 
     /// Removes the node with the given identifier from the graph.
@@ -680,10 +610,7 @@
     ///
     /// storage.remove_node(&a).unwrap();
     /// ```
-    fn remove_node(
-        &mut self,
-        id: Self::NodeId,
-    ) -> Option<DetachedNode<Self::NodeId, Self::NodeWeight>>;
+    fn remove_node(&mut self, id: NodeId) -> Option<DetachedNode<Self::NodeWeight>>;
 
     /// Removes the edge with the given identifier from the graph.
     ///
@@ -714,10 +641,7 @@
     /// #
     /// # assert_eq!(storage.num_edges(), 0);
     /// ```
-    fn remove_edge(
-        &mut self,
-        id: Self::EdgeId,
-    ) -> Option<DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>>;
+    fn remove_edge(&mut self, id: EdgeId) -> Option<DetachedEdge<Self::EdgeWeight>>;
 
     /// Clears the graph, removing all nodes and edges.
     ///
@@ -773,7 +697,7 @@
     /// // This will access the underlying storage, and is equivalent to `storage.neighbours(&a)`.
     /// assert_eq!(node.neighbours().count(), 0);
     /// ```
-    fn node(&self, id: Self::NodeId) -> Option<Node<Self>>;
+    fn node(&self, id: NodeId) -> Option<Node<Self>>;
 
     /// Returns the node, with a mutable weight, with the given identifier.
     ///
@@ -797,7 +721,7 @@
     /// assert_eq!(node.id(), &a);
     /// assert_eq!(node.weight(), &mut 1);
     /// ```
-    fn node_mut(&mut self, id: Self::NodeId) -> Option<NodeMut<Self>>;
+    fn node_mut(&mut self, id: NodeId) -> Option<NodeMut<Self>>;
 
     /// Checks if the node with the given identifier exists.
     ///
@@ -825,7 +749,7 @@
     /// The default implementation simply checks if [`Self::node`] returns [`Some`], but if
     /// possible, custom implementations that are able to do this more efficiently should override
     /// this.
-    fn contains_node(&self, id: Self::NodeId) -> bool {
+    fn contains_node(&self, id: NodeId) -> bool {
         self.node(id).is_some()
     }
 
@@ -872,7 +796,7 @@
     /// assert_eq!(edge.target().id(), &b);
     /// assert_eq!(edge.target().weight(), &2);
     /// ```
-    fn edge(&self, id: Self::EdgeId) -> Option<Edge<Self>>;
+    fn edge(&self, id: EdgeId) -> Option<Edge<Self>>;
 
     /// Returns the edge, with a mutable weight, with the given identifier, if it exists.
     ///
@@ -905,7 +829,7 @@
     ///
     /// assert_eq!(storage.edge(&ab).unwrap().weight(), &4);
     /// ```
-    fn edge_mut(&mut self, id: Self::EdgeId) -> Option<EdgeMut<Self>>;
+    fn edge_mut(&mut self, id: EdgeId) -> Option<EdgeMut<Self>>;
 
     /// Checks if the edge with the given identifier exists.
     ///
@@ -938,7 +862,7 @@
     /// The default implementation simply checks if [`Self::edge`] returns [`Some`], but if
     /// possible, custom implementations that are able to do this more efficiently should override
     /// this.
-    fn contains_edge(&self, id: Self::EdgeId) -> bool {
+    fn contains_edge(&self, id: EdgeId) -> bool {
         self.edge(id).is_some()
     }
 
@@ -981,11 +905,7 @@
     /// The default implementation simply calls [`Self::node_connections`] on both nodes and then
     /// chains those, after filtering for the respective end. Most implementations should be able to
     /// provide a more efficient implementation.
-    fn edges_between(
-        &self,
-        u: Self::NodeId,
-        v: Self::NodeId,
-    ) -> impl Iterator<Item = Edge<'_, Self>> {
+    fn edges_between(&self, u: NodeId, v: NodeId) -> impl Iterator<Item = Edge<'_, Self>> {
         // How does this work with a default implementation?
         let from_source = self.node_connections(u).filter(move |edge| {
             let (edge_u, edge_v) = edge.endpoint_ids();
@@ -1052,8 +972,8 @@
     // I'd love to provide a default implementation for this, but I just can't get it to work.
     fn edges_between_mut(
         &mut self,
-        u: Self::NodeId,
-        v: Self::NodeId,
+        u: NodeId,
+        v: NodeId,
     ) -> impl Iterator<Item = EdgeMut<'_, Self>>;
 
     /// Returns an iterator over all edges that are connected to the given node.
@@ -1094,7 +1014,7 @@
     ///     [ab, ca]
     /// );
     /// ```
-    fn node_connections(&self, id: Self::NodeId) -> impl Iterator<Item = Edge<'_, Self>>;
+    fn node_connections(&self, id: NodeId) -> impl Iterator<Item = Edge<'_, Self>>;
 
     /// Returns an iterator over all edges that are connected to the given node, with mutable
     /// weights.
@@ -1139,8 +1059,7 @@
     ///     [5, 6]
     /// );
     /// ```
-    fn node_connections_mut(&mut self, id: Self::NodeId)
-    -> impl Iterator<Item = EdgeMut<'_, Self>>;
+    fn node_connections_mut(&mut self, id: NodeId) -> impl Iterator<Item = EdgeMut<'_, Self>>;
 
     /// Returns the number of edges that are connected to the given node.
     ///
@@ -1176,7 +1095,7 @@
     /// edges.
     /// This is unlikely to be the most efficient implementation, so custom implementations should
     /// override this.
-    fn node_degree(&self, id: Self::NodeId) -> usize {
+    fn node_degree(&self, id: NodeId) -> usize {
         self.node_connections(id).count()
     }
 
@@ -1234,7 +1153,7 @@
     ///
     /// Changing the requirement from **SHOULD NOT** to **MUST NOT** may occur in the future, and is
     /// to be considered a breaking change.
-    fn node_neighbours(&self, id: Self::NodeId) -> impl Iterator<Item = Node<'_, Self>> {
+    fn node_neighbours(&self, id: NodeId) -> impl Iterator<Item = Node<'_, Self>> {
         self.node_connections(id)
             .filter_map(move |edge: Edge<Self>| {
                 let (u, v) = edge.endpoint_ids();
@@ -1295,7 +1214,7 @@
     ///
     /// No default implementation is provided, as a mutable iterator based on
     /// [`Self::node_connections_mut`] could potentially lead to a double mutable borrow.
-    fn node_neighbours_mut(&mut self, id: Self::NodeId) -> impl Iterator<Item = NodeMut<'_, Self>>;
+    fn node_neighbours_mut(&mut self, id: NodeId) -> impl Iterator<Item = NodeMut<'_, Self>>;
 
     /// Returns an iterator over all nodes that do not have any edges connected to them.
     ///
diff --git a/crates/core/src/storage/reverse.rs b/crates/core/src/storage/reverse.rs
new file mode 100644
index 0000000..11ebb54
--- /dev/null
+++ b/crates/core/src/storage/reverse.rs
@@ -0,0 +1,18 @@
+use crate::{Edge, EdgeMut, GraphStorage, Node, NodeMut};
+
+pub trait ReverseGraphStorage: GraphStorage {
+    type NodeKey;
+    type EdgeKey;
+
+    fn contains_node_key(&self, key: &Self::NodeKey) -> bool {
+        self.node_by_key(key).is_some()
+    }
+    fn node_by_key(&self, key: &Self::NodeKey) -> Option<Node<Self>>;
+    fn node_by_key_mut(&mut self, key: &Self::NodeKey) -> Option<NodeMut<Self>>;
+
+    fn contains_edge_key(&self, key: &Self::EdgeKey) -> bool {
+        self.edge_by_key(key).is_some()
+    }
+    fn edge_by_key(&self, key: &Self::EdgeKey) -> Option<Edge<Self>>;
+    fn edge_by_key_mut(&mut self, key: &Self::EdgeKey) -> Option<EdgeMut<Self>>;
+}
diff --git a/crates/core/src/id/linear.rs b/crates/core/src/storage/sequential.rs
similarity index 78%
rename from crates/core/src/id/linear.rs
rename to crates/core/src/storage/sequential.rs
index f36e349..e64dd04 100644
--- a/crates/core/src/id/linear.rs
+++ b/crates/core/src/storage/sequential.rs
@@ -1,6 +1,4 @@
-use numi::borrow::Moo;
-
-use crate::{id::GraphId, storage::GraphStorage};
+use crate::{edge::EdgeId, node::NodeId};
 
 /// Index mapper for a graph.
 ///
@@ -16,7 +14,7 @@
 /// input value should always map to the same output value.
 /// Index lookup should also be (if possible) `O(1)` for `Id -> usize`, but not necessarily for
 /// `usize -> Id`.
-pub trait IndexMapper<Id> {
+pub trait GraphIdBijection<Id> {
     /// The maximum value that can be mapped to.
     ///
     /// This **must** be equal to the number of nodes in the graph.
@@ -105,14 +103,13 @@
     ///
     /// ```
     /// use numi::borrow::Moo;
-    /// use petgraph_core::id::{IndexMapper, LinearGraphId};
     /// use petgraph_dino::{DiDinoGraph, NodeId};
     ///
     /// let mut graph = DiDinoGraph::new();
     ///
     /// let a = *graph.insert_node("A").id();
     /// let b = *graph.insert_node("B").id();
-    /// # let ab = graph.insert_edge("A → B", &a, &b);
+    /// # let ab = graph.insert_edge("A → B", a, b);
     ///
     /// let mut mapper = NodeId::index_mapper(graph.storage());
     ///
@@ -122,34 +119,15 @@
     fn reverse(&self, to: usize) -> Option<Id>;
 }
 
-/// Linear graph identifier.
-///
-/// A linear graph identifier is a graph identifier that has a linear mapping to a `usize` value,
-/// that mapping must be continuous .
-pub trait LinearGraphId<S>: GraphId + Sized
-where
-    S: GraphStorage,
-{
-    /// The index mapper for this graph identifier.
-    type Mapper<'graph>: IndexMapper<Self>
+pub trait SequentialGraphStorage {
+    type EdgeIdBijection<'graph>: GraphIdBijection<EdgeId>
     where
-        S: 'graph;
+        Self: 'graph;
 
-    /// Get the index mapper for this graph identifier.
-    ///
-    /// # Example
-    ///
-    /// ```
-    /// use petgraph_core::id::{IndexMapper, LinearGraphId};
-    /// use petgraph_dino::{DiDinoGraph, NodeId};
-    ///
-    /// let mut graph = DiDinoGraph::new();
-    ///
-    /// let a = *graph.insert_node("A").id();
-    /// let b = *graph.insert_node("B").id();
-    /// # let ab = graph.insert_edge("A → B", &a, &b);
-    ///
-    /// let mapper = NodeId::index_mapper(graph.storage());
-    /// ```
-    fn index_mapper(storage: &S) -> Self::Mapper<'_>;
+    type NodeIdBijection<'graph>: GraphIdBijection<NodeId>
+    where
+        Self: 'graph;
+
+    fn node_id_bijection(&self) -> Self::NodeIdBijection<'_>;
+    fn edge_id_bijection(&self) -> Self::EdgeIdBijection<'_>;
 }
diff --git a/crates/dino/src/auxiliary.rs b/crates/dino/src/auxiliary.rs
new file mode 100644
index 0000000..82aa1ee
--- /dev/null
+++ b/crates/dino/src/auxiliary.rs
@@ -0,0 +1,37 @@
+use petgraph_core::{
+    edge::EdgeId,
+    node::NodeId,
+    storage::{auxiliary::Hints, AuxiliaryGraphStorage},
+    GraphDirectionality,
+};
+
+use crate::{
+    slab::secondary::{SlabBooleanStorage, SlabSecondaryStorage},
+    DinoStorage,
+};
+
+impl<N, E, D> AuxiliaryGraphStorage for DinoStorage<N, E, D>
+where
+    D: GraphDirectionality,
+{
+    type BooleanEdgeStorage<'graph> = SlabBooleanStorage<'graph, EdgeId> where Self: 'graph;
+    type BooleanNodeStorage<'graph> = SlabBooleanStorage<'graph, NodeId>  where Self: 'graph;
+    type SecondaryEdgeStorage<'graph, V> = SlabSecondaryStorage<'graph, EdgeId, V>  where Self: 'graph;
+    type SecondaryNodeStorage<'graph, V> = SlabSecondaryStorage<'graph, NodeId, V> where Self: 'graph;
+
+    fn secondary_node_storage<V>(&self, _: Hints) -> Self::SecondaryNodeStorage<'_, V> {
+        SlabSecondaryStorage::new(&self.nodes)
+    }
+
+    fn secondary_edge_storage<V>(&self, _: Hints) -> Self::SecondaryEdgeStorage<'_, V> {
+        SlabSecondaryStorage::new(&self.edges)
+    }
+
+    fn boolean_node_storage(&self, _: Hints) -> Self::BooleanNodeStorage<'_> {
+        SlabBooleanStorage::new(&self.nodes)
+    }
+
+    fn boolean_edge_storage(&self, _: Hints) -> Self::BooleanEdgeStorage<'_> {
+        SlabBooleanStorage::new(&self.edges)
+    }
+}
diff --git a/crates/dino/src/closure/mod.rs b/crates/dino/src/closure/mod.rs
index fa2f55a..d3bb255 100644
--- a/crates/dino/src/closure/mod.rs
+++ b/crates/dino/src/closure/mod.rs
@@ -4,7 +4,7 @@
 use crate::{
     edge::{Edge, EdgeSlab},
     node::{Node, NodeSlab},
-    EdgeId, NodeId,
+    NodeId,
 };
 
 #[derive(Debug, Copy, Clone, PartialEq)]
@@ -116,13 +116,12 @@
 
     use hashbrown::{HashMap, HashSet};
     use petgraph_core::{
-        attributes::Attributes, edge::marker::Directed, GraphDirectionality, GraphStorage,
+        edge::{marker::Directed, EdgeId},
+        node::NodeId,
+        GraphDirectionality,
     };
 
-    use crate::{
-        slab::{EntryId, Key as _},
-        DinoGraph, DinoStorage, EdgeId, NodeId,
-    };
+    use crate::{DinoGraph, DinoStorage};
 
     #[derive(Debug, Clone, PartialEq, Eq)]
     pub(crate) struct EvaluatedNodeClosure {
@@ -233,7 +232,7 @@
         let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
         let node = graph.try_insert_node(1).unwrap();
-        let id = *node.id();
+        let id = node.id();
 
         assert_eq!(isolated(&graph), once(id).collect());
 
@@ -285,11 +284,11 @@
     fn multiple_nodes() {
         let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
-        let a = graph.try_insert_node(Attributes::new(1)).unwrap();
-        let a = *a.id();
+        let a = graph.try_insert_node(1).unwrap();
+        let a = a.id();
 
-        let b = graph.try_insert_node(Attributes::new(2)).unwrap();
-        let b = *b.id();
+        let b = graph.try_insert_node(2).unwrap();
+        let b = b.id();
 
         assert_eq!(isolated(&graph), [a, b].into_iter().collect());
 
@@ -346,13 +345,13 @@
         let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
         let a = graph.try_insert_node(1u8).unwrap();
-        let a = *a.id();
+        let a = a.id();
 
         let b = graph.try_insert_node(1u8).unwrap();
-        let b = *b.id();
+        let b = b.id();
 
-        let edge = graph.try_insert_edge(1u8, &a, &b).unwrap();
-        let edge = *edge.id();
+        let edge = graph.try_insert_edge(1u8, a, b).unwrap();
+        let edge = edge.id();
 
         assert!(isolated(&graph).is_empty());
 
@@ -411,10 +410,10 @@
         let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
         let a = graph.try_insert_node(1u8).unwrap();
-        let a = *a.id();
+        let a = a.id();
 
-        let edge = graph.try_insert_edge(1u8, &a, &a).unwrap();
-        let edge = *edge.id();
+        let edge = graph.try_insert_edge(1u8, a, a).unwrap();
+        let edge = edge.id();
 
         assert!(isolated(&graph).is_empty());
 
@@ -469,22 +468,22 @@
             let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
             let a = graph.try_insert_node(1u8).unwrap();
-            let a = *a.id();
+            let a = a.id();
 
             let b = graph.try_insert_node(1u8).unwrap();
-            let b = *b.id();
+            let b = b.id();
 
             let c = graph.try_insert_node(1u8).unwrap();
-            let c = *c.id();
+            let c = c.id();
 
-            let ab = graph.try_insert_edge(1u8, &a, &b).unwrap();
-            let ab = *ab.id();
+            let ab = graph.try_insert_edge(1u8, a, b).unwrap();
+            let ab = ab.id();
 
-            let bc = graph.try_insert_edge(1u8, &b, &c).unwrap();
-            let bc = *bc.id();
+            let bc = graph.try_insert_edge(1u8, b, c).unwrap();
+            let bc = bc.id();
 
-            let ca = graph.try_insert_edge(1u8, &c, &a).unwrap();
-            let ca = *ca.id();
+            let ca = graph.try_insert_edge(1u8, c, a).unwrap();
+            let ca = ca.id();
 
             Self {
                 graph,
@@ -592,16 +591,16 @@
         let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
         let a = graph.try_insert_node(1u8).unwrap();
-        let a = *a.id();
+        let a = a.id();
 
         let b = graph.try_insert_node(1u8).unwrap();
-        let b = *b.id();
+        let b = b.id();
 
-        let ab1 = graph.try_insert_edge(1u8, &a, &b).unwrap();
-        let ab1 = *ab1.id();
+        let ab1 = graph.try_insert_edge(1u8, a, b).unwrap();
+        let ab1 = ab1.id();
 
-        let ab2 = graph.try_insert_edge(1u8, &a, &b).unwrap();
-        let ab2 = *ab2.id();
+        let ab2 = graph.try_insert_edge(1u8, a, b).unwrap();
+        let ab2 = ab2.id();
 
         assert!(isolated(&graph).is_empty());
 
@@ -669,7 +668,7 @@
             ..
         } = graph;
 
-        graph.remove_node(&b).unwrap();
+        graph.remove_node(b).unwrap();
 
         assert!(isolated(&graph).is_empty());
 
@@ -738,7 +737,7 @@
             ca,
         } = graph;
 
-        graph.remove_edge(&bc).unwrap();
+        graph.remove_edge(bc).unwrap();
 
         assert!(isolated(&graph).is_empty());
 
diff --git a/crates/dino/src/directed.rs b/crates/dino/src/directed.rs
index 7ae82ac..5759d2c 100644
--- a/crates/dino/src/directed.rs
+++ b/crates/dino/src/directed.rs
@@ -1,14 +1,14 @@
 use either::Either;
 use petgraph_core::{
-    edge::{Direction, Edge, EdgeMut},
-    node::{Node, NodeMut},
+    edge::{Direction, Edge, EdgeId, EdgeMut},
+    node::{Node, NodeId, NodeMut},
     storage::{DirectedGraphStorage, GraphStorage},
 };
 
 use crate::{
     iter::directed::NodeDirectedConnectionsIter,
     node::{NodeClosures, NodeSlab},
-    DinoStorage, Directed, EdgeId, NodeId,
+    DinoStorage, Directed,
 };
 
 fn directed_edges_between<N>(
@@ -28,16 +28,16 @@
 impl<N, E> DirectedGraphStorage for DinoStorage<N, E, Directed> {
     fn directed_edges_between(
         &self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = Edge<Self>> {
         directed_edges_between(&self.nodes, source, target).filter_map(move |id| self.edge(id))
     }
 
     fn directed_edges_between_mut(
         &mut self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = EdgeMut<Self>> {
         let Self { edges, nodes, .. } = self;
 
@@ -50,7 +50,7 @@
 
     fn node_directed_connections(
         &self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Edge<Self>> {
         NodeDirectedConnectionsIter {
@@ -64,7 +64,7 @@
 
     fn node_directed_connections_mut(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = EdgeMut<Self>> {
         let Self { nodes, edges, .. } = self;
@@ -84,7 +84,7 @@
 
     fn node_directed_neighbours(
         &self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = Node<Self>> {
         self.nodes
@@ -99,7 +99,7 @@
 
     fn node_directed_neighbours_mut(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
         direction: Direction,
     ) -> impl Iterator<Item = NodeMut<Self>> {
         let Some(node) = self.nodes.get(id) else {
diff --git a/crates/dino/src/edge.rs b/crates/dino/src/edge.rs
index 3442d2c..a9d3bd6 100644
--- a/crates/dino/src/edge.rs
+++ b/crates/dino/src/edge.rs
@@ -1,97 +1,19 @@
-use core::fmt::{Display, Formatter};
+use petgraph_core::{edge::EdgeId, node::NodeId};
 
-use petgraph_core::{
-    attributes::NoValue,
-    edge::marker::GraphDirectionality,
-    id::{AssociativeGraphId, GraphId, LinearGraphId, ManagedGraphId},
-};
-
-use crate::{
-    node::NodeId,
-    slab::{
-        secondary::{SlabAttributeMapper, SlabBooleanMapper},
-        EntryId, Key, SlabIndexMapper,
-    },
-    DinoStorage,
-};
-
-/// Identifier for an edge in [`DinoStorage`].
-///
-/// [`EdgeId`] is a unique identifier for an edge in a [`DinoStorage`].
-/// It is used to reference edges within the graph.
-///
-/// An [`EdgeId`] is managed, meaning that it is chosen by the graph itself and not by the user.
-///
-/// [`EdgeId`] implements [`GraphId`], [`ManagedGraphId`] and [`LinearGraphId`].
-///
-/// # Example
-///
-/// ```
-/// use petgraph_dino::DiDinoGraph;
-///
-/// let mut graph = DiDinoGraph::new();
-///
-/// let a = *graph.insert_node("A").id();
-/// let b = *graph.insert_node("B").id();
-///
-/// let ab = *graph.insert_edge("A → B", &a, &b).id();
-///
-/// println!("Edge A → B: {ab}");
-/// ```
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct EdgeId(EntryId);
-
-impl Display for EdgeId {
-    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
-        Display::fmt(&self.0, f)
-    }
-}
+use crate::slab::{EntryId, Key};
 
 impl Key for EdgeId {
     #[inline]
     fn from_id(id: EntryId) -> Self {
-        Self(id)
+        Self::new(id.into_usize())
     }
 
     #[inline]
     fn into_id(self) -> EntryId {
-        self.0
+        EntryId::new_unchecked(self.into_inner())
     }
 }
 
-impl GraphId for EdgeId {
-    type AttributeIndex = NoValue;
-}
-
-impl<N, E, D> LinearGraphId<DinoStorage<N, E, D>> for EdgeId
-where
-    D: GraphDirectionality,
-{
-    type Mapper<'a> = SlabIndexMapper<'a, Self> where Self: 'a, N: 'a, E: 'a;
-
-    fn index_mapper(storage: &DinoStorage<N, E, D>) -> Self::Mapper<'_> {
-        SlabIndexMapper::new(&storage.edges)
-    }
-}
-
-impl<N, E, D> AssociativeGraphId<DinoStorage<N, E, D>> for EdgeId
-where
-    D: GraphDirectionality,
-{
-    type AttributeMapper<'a, V> = SlabAttributeMapper<'a, Self, V> where DinoStorage<N, E, D>: 'a;
-    type BooleanMapper<'a> = SlabBooleanMapper<'a> where DinoStorage<N, E, D>: 'a;
-
-    fn attribute_mapper<V>(storage: &DinoStorage<N, E, D>) -> Self::AttributeMapper<'_, V> {
-        SlabAttributeMapper::new(&storage.edges)
-    }
-
-    fn boolean_mapper(storage: &DinoStorage<N, E, D>) -> Self::BooleanMapper<'_> {
-        SlabBooleanMapper::new(&storage.edges)
-    }
-}
-
-impl ManagedGraphId for EdgeId {}
-
 pub(crate) type EdgeSlab<T> = crate::slab::Slab<EdgeId, Edge<T>>;
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
diff --git a/crates/dino/src/iter/closure.rs b/crates/dino/src/iter/closure.rs
index b601da7..4b34c31 100644
--- a/crates/dino/src/iter/closure.rs
+++ b/crates/dino/src/iter/closure.rs
@@ -1,6 +1,8 @@
 use core::{cmp::Ordering, iter::Peekable};
 
-use crate::{node::NodeClosures, EdgeId, NodeId};
+use petgraph_core::{edge::EdgeId, node::NodeId};
+
+use crate::node::NodeClosures;
 
 pub(crate) type NodeIdClosureIter<'a> = core::iter::Copied<core::slice::Iter<'a, NodeId>>;
 pub(crate) type EdgeIdClosureIter<'a> = core::iter::Copied<core::slice::Iter<'a, EdgeId>>;
diff --git a/crates/dino/src/iter/directed.rs b/crates/dino/src/iter/directed.rs
index a6a28e8..c237225 100644
--- a/crates/dino/src/iter/directed.rs
+++ b/crates/dino/src/iter/directed.rs
@@ -1,6 +1,6 @@
-use petgraph_core::{Edge, GraphDirectionality};
+use petgraph_core::{edge::EdgeId, Edge, GraphDirectionality};
 
-use crate::{DinoStorage, EdgeId};
+use crate::DinoStorage;
 
 pub(crate) struct NodeDirectedConnectionsIter<'storage, N, E, D, I>
 where
diff --git a/crates/dino/src/lib.rs b/crates/dino/src/lib.rs
index 54e3b30..a33a314 100644
--- a/crates/dino/src/lib.rs
+++ b/crates/dino/src/lib.rs
@@ -111,10 +111,12 @@
 
 extern crate alloc;
 
+mod auxiliary;
 pub(crate) mod closure;
 mod directed;
 mod edge;
 mod iter;
+mod linear;
 mod node;
 mod retain;
 pub(crate) mod slab;
@@ -123,17 +125,14 @@
 
 use core::fmt::{Debug, Display};
 
-pub use edge::EdgeId;
 use either::Either;
 use error_stack::{Context, Report, Result};
-pub use node::NodeId;
 use petgraph_core::{
     edge::{
         marker::{Directed, GraphDirectionality, Undirected},
-        DetachedEdge, EdgeMut,
+        DetachedEdge, EdgeId, EdgeMut,
     },
-    id::GraphId,
-    node::{DetachedNode, NodeMut},
+    node::{DetachedNode, NodeId, NodeMut},
     storage::GraphStorage,
     Graph,
 };
@@ -334,7 +333,7 @@
 ///
 /// let ab = *graph.insert_edge(Edge, &a, &b).id();
 /// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DinoStorage<N, E, D = Directed>
 where
     D: GraphDirectionality,
@@ -435,10 +434,8 @@
 where
     D: GraphDirectionality,
 {
-    type EdgeId = EdgeId;
     type EdgeWeight = E;
     type Error = Error;
-    type NodeId = NodeId;
     type NodeWeight = N;
 
     fn with_capacity(node_capacity: Option<usize>, edge_capacity: Option<usize>) -> Self {
@@ -451,46 +448,23 @@
     }
 
     fn from_parts(
-        nodes: impl IntoIterator<Item = DetachedNode<Self::NodeId, Self::NodeWeight>>,
-        edges: impl IntoIterator<Item = DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>>,
+        nodes: impl IntoIterator<Item = DetachedNode<Self::NodeWeight>>,
+        edges: impl IntoIterator<Item = DetachedEdge<Self::EdgeWeight>>,
     ) -> Result<Self, Self::Error> {
-        let mut nodes: Slab<_, _> = nodes
-            .into_iter()
-            .map(|node: DetachedNode<Self::NodeId, Self::NodeWeight>| {
-                (node.id, Node::new(node.id, node.weight))
-            })
-            .collect();
-
-        let edges: Slab<_, _> = edges
-            .into_iter()
-            .map(
-                |edge: DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>| {
-                    (edge.id, Edge::new(edge.id, edge.weight, edge.u, edge.v))
-                },
-            )
-            .collect();
+        todo!();
 
         // TODO: test-case c:
         // TODO: this doesn't work if we remove a node
         // TODO: NodeId rename is not of concern for us though
         // TODO: what about nodes that are added or edges?
         //      We don't know their ID yet (need a way to get those -> PartialNode/Edge)
-
-        Closures::refresh(&mut nodes, &edges);
-
-        Ok(Self {
-            nodes,
-            edges,
-
-            _marker: core::marker::PhantomData,
-        })
     }
 
     fn into_parts(
         self,
     ) -> (
-        impl Iterator<Item = DetachedNode<Self::NodeId, Self::NodeWeight>>,
-        impl Iterator<Item = DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>>,
+        impl Iterator<Item = DetachedNode<Self::NodeWeight>>,
+        impl Iterator<Item = DetachedEdge<Self::EdgeWeight>>,
     ) {
         let nodes = self.nodes.into_iter().map(|node| DetachedNode {
             id: node.id,
@@ -515,13 +489,13 @@
         self.edges.len()
     }
 
-    fn next_node_id(&self, _: <Self::NodeId as GraphId>::AttributeIndex) -> Self::NodeId {
+    fn next_node_id(&self) -> NodeId {
         self.nodes.next_key()
     }
 
     fn insert_node(
         &mut self,
-        id: Self::NodeId,
+        id: NodeId,
         weight: Self::NodeWeight,
     ) -> Result<NodeMut<Self>, Self::Error> {
         let expected = id;
@@ -545,17 +519,17 @@
         Ok(NodeMut::new(node.id, &mut node.weight))
     }
 
-    fn next_edge_id(&self, _: <Self::EdgeId as GraphId>::AttributeIndex) -> Self::EdgeId {
+    fn next_edge_id(&self) -> EdgeId {
         self.edges.next_key()
     }
 
     fn insert_edge(
         &mut self,
-        id: Self::EdgeId,
+        id: EdgeId,
         weight: Self::EdgeWeight,
 
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> Result<EdgeMut<Self>, Self::Error> {
         // TODO: option to disallow self-loops and parallel edges
 
@@ -599,10 +573,7 @@
         ))
     }
 
-    fn remove_node(
-        &mut self,
-        id: Self::NodeId,
-    ) -> Option<DetachedNode<Self::NodeId, Self::NodeWeight>> {
+    fn remove_node(&mut self, id: NodeId) -> Option<DetachedNode<Self::NodeWeight>> {
         let node = self.nodes.remove(id)?;
 
         for edge in node.closures.edges() {
@@ -616,10 +587,7 @@
         Some(DetachedNode::new(id, weight))
     }
 
-    fn remove_edge(
-        &mut self,
-        id: Self::EdgeId,
-    ) -> Option<DetachedEdge<Self::EdgeId, Self::NodeId, Self::EdgeWeight>> {
+    fn remove_edge(&mut self, id: EdgeId) -> Option<DetachedEdge<Self::EdgeWeight>> {
         let edge = self.edges.remove(id)?;
         Closures::remove_edge(&edge, &mut self.nodes);
 
@@ -637,42 +605,42 @@
         Closures::clear(&mut self.nodes);
     }
 
-    fn node(&self, id: Self::NodeId) -> Option<petgraph_core::node::Node<Self>> {
+    fn node(&self, id: NodeId) -> Option<petgraph_core::node::Node<Self>> {
         self.nodes
             .get(id)
             .map(|node| petgraph_core::node::Node::new(self, node.id, &node.weight))
     }
 
-    fn node_mut(&mut self, id: Self::NodeId) -> Option<NodeMut<Self>> {
+    fn node_mut(&mut self, id: NodeId) -> Option<NodeMut<Self>> {
         self.nodes
             .get_mut(id)
             .map(|node| NodeMut::new(node.id, &mut node.weight))
     }
 
-    fn contains_node(&self, id: Self::NodeId) -> bool {
+    fn contains_node(&self, id: NodeId) -> bool {
         self.nodes.contains_key(id)
     }
 
-    fn edge(&self, id: Self::EdgeId) -> Option<petgraph_core::edge::Edge<Self>> {
+    fn edge(&self, id: EdgeId) -> Option<petgraph_core::edge::Edge<Self>> {
         self.edges.get(id).map(|edge| {
             petgraph_core::edge::Edge::new(self, edge.id, &edge.weight, edge.source, edge.target)
         })
     }
 
-    fn edge_mut(&mut self, id: Self::EdgeId) -> Option<EdgeMut<Self>> {
+    fn edge_mut(&mut self, id: EdgeId) -> Option<EdgeMut<Self>> {
         self.edges
             .get_mut(id)
             .map(|edge| EdgeMut::new(edge.id, &mut edge.weight, edge.source, edge.target))
     }
 
-    fn contains_edge(&self, id: Self::EdgeId) -> bool {
+    fn contains_edge(&self, id: EdgeId) -> bool {
         self.edges.contains_key(id)
     }
 
     fn edges_between(
         &self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = petgraph_core::edge::Edge<Self>> {
         edges_between_undirected(&self.nodes, source, target)
             .filter_map(move |edge| self.edge(edge))
@@ -680,8 +648,8 @@
 
     fn edges_between_mut(
         &mut self,
-        source: Self::NodeId,
-        target: Self::NodeId,
+        source: NodeId,
+        target: NodeId,
     ) -> impl Iterator<Item = EdgeMut<Self>> {
         let available = edges_between_undirected(&self.nodes, source, target);
 
@@ -692,7 +660,7 @@
 
     fn node_connections(
         &self,
-        id: Self::NodeId,
+        id: NodeId,
     ) -> impl Iterator<Item = petgraph_core::edge::Edge<Self>> {
         self.nodes
             .get(id)
@@ -701,7 +669,7 @@
             .filter_map(move |edge| self.edge(edge))
     }
 
-    fn node_connections_mut(&mut self, id: Self::NodeId) -> impl Iterator<Item = EdgeMut<Self>> {
+    fn node_connections_mut(&mut self, id: NodeId) -> impl Iterator<Item = EdgeMut<Self>> {
         let Self { nodes, edges, .. } = self;
 
         let available = nodes
@@ -714,10 +682,7 @@
             .map(move |edge| EdgeMut::new(edge.id, &mut edge.weight, edge.source, edge.target))
     }
 
-    fn node_neighbours(
-        &self,
-        id: Self::NodeId,
-    ) -> impl Iterator<Item = petgraph_core::node::Node<Self>> {
+    fn node_neighbours(&self, id: NodeId) -> impl Iterator<Item = petgraph_core::node::Node<Self>> {
         self.nodes
             .get(id)
             .into_iter()
@@ -725,7 +690,7 @@
             .filter_map(move |node| self.node(node))
     }
 
-    fn node_neighbours_mut(&mut self, id: Self::NodeId) -> impl Iterator<Item = NodeMut<Self>> {
+    fn node_neighbours_mut(&mut self, id: NodeId) -> impl Iterator<Item = NodeMut<Self>> {
         let Some(node) = self.nodes.get(id) else {
             return Either::Right(core::iter::empty());
         };
diff --git a/crates/dino/src/linear.rs b/crates/dino/src/linear.rs
new file mode 100644
index 0000000..a1742b2
--- /dev/null
+++ b/crates/dino/src/linear.rs
@@ -0,0 +1,21 @@
+use petgraph_core::{
+    edge::EdgeId, node::NodeId, storage::sequential::SequentialGraphStorage, GraphDirectionality,
+};
+
+use crate::{slab::SlabIndexMapper, DinoStorage};
+
+impl<N, E, D> SequentialGraphStorage for DinoStorage<N, E, D>
+where
+    D: GraphDirectionality,
+{
+    type EdgeIdBijection<'graph> = SlabIndexMapper<'graph, EdgeId> where Self: 'graph;
+    type NodeIdBijection<'graph> = SlabIndexMapper<'graph, NodeId> where Self: 'graph;
+
+    fn node_id_bijection(&self) -> Self::NodeIdBijection<'_> {
+        SlabIndexMapper::new(&self.nodes)
+    }
+
+    fn edge_id_bijection(&self) -> Self::EdgeIdBijection<'_> {
+        SlabIndexMapper::new(&self.edges)
+    }
+}
diff --git a/crates/dino/src/node.rs b/crates/dino/src/node.rs
index 88818da..abca212 100644
--- a/crates/dino/src/node.rs
+++ b/crates/dino/src/node.rs
@@ -1,10 +1,4 @@
-use core::fmt::{Display, Formatter};
-
-use petgraph_core::{
-    attributes::NoValue,
-    edge::marker::GraphDirectionality,
-    id::{AssociativeGraphId, GraphId, LinearGraphId, ManagedGraphId},
-};
+use petgraph_core::{edge::EdgeId, node::NodeId};
 
 use crate::{
     closure::UniqueVec,
@@ -12,87 +6,21 @@
         EdgeBetweenIterator, EdgeIdClosureIter, EdgeIntersectionIterator, EdgeIterator,
         NeighbourIterator, NodeIdClosureIter,
     },
-    slab::{
-        secondary::{SlabAttributeMapper, SlabBooleanMapper},
-        EntryId, Key, SlabIndexMapper,
-    },
-    DinoStorage, EdgeId,
+    slab::{EntryId, Key},
 };
 
-/// Identifier for a node in [`DinoStorage`].
-///
-/// [`NodeId`] is a unique identifier for a node in a [`DinoStorage`].
-/// It is used to reference nodes within the graph.
-///
-/// A [`NodeId`] is managed, meaning that it is chosen by the graph itself and not by the user.
-///
-/// [`NodeId`] implements [`GraphId`], [`ManagedGraphId`] and [`LinearGraphId`].
-///
-/// # Example
-///
-/// ```
-/// use petgraph_dino::DiDinoGraph;
-///
-/// let mut graph = DiDinoGraph::<_, u8>::new();
-///
-/// let a = *graph.insert_node("A").id();
-///
-/// println!("Node A: {a}");
-/// ```
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct NodeId(EntryId);
-
-impl Display for NodeId {
-    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
-        Display::fmt(&self.0, f)
-    }
-}
-
 impl Key for NodeId {
     #[inline]
     fn from_id(id: EntryId) -> Self {
-        Self(id)
+        Self::new(id.into_usize())
     }
 
     #[inline]
     fn into_id(self) -> EntryId {
-        self.0
+        EntryId::new_unchecked(self.into_inner())
     }
 }
 
-impl GraphId for NodeId {
-    type AttributeIndex = NoValue;
-}
-
-impl<N, E, D> LinearGraphId<DinoStorage<N, E, D>> for NodeId
-where
-    D: GraphDirectionality,
-{
-    type Mapper<'a> = SlabIndexMapper<'a, Self> where Self: 'a, N: 'a, E: 'a;
-
-    fn index_mapper(storage: &DinoStorage<N, E, D>) -> Self::Mapper<'_> {
-        SlabIndexMapper::new(&storage.nodes)
-    }
-}
-
-impl<N, E, D> AssociativeGraphId<DinoStorage<N, E, D>> for NodeId
-where
-    D: GraphDirectionality,
-{
-    type AttributeMapper<'a, V> = SlabAttributeMapper<'a, Self, V> where DinoStorage<N, E, D>: 'a;
-    type BooleanMapper<'a> = SlabBooleanMapper<'a> where DinoStorage<N, E, D>: 'a;
-
-    fn attribute_mapper<V>(storage: &DinoStorage<N, E, D>) -> Self::AttributeMapper<'_, V> {
-        SlabAttributeMapper::new(&storage.nodes)
-    }
-
-    fn boolean_mapper(storage: &DinoStorage<N, E, D>) -> Self::BooleanMapper<'_> {
-        SlabBooleanMapper::new(&storage.nodes)
-    }
-}
-
-impl ManagedGraphId for NodeId {}
-
 pub(crate) type NodeSlab<T> = crate::slab::Slab<NodeId, Node<T>>;
 
 #[derive(Debug, Clone)]
diff --git a/crates/dino/src/slab/id.rs b/crates/dino/src/slab/id.rs
index 8f17995..3bbe8fc 100644
--- a/crates/dino/src/slab/id.rs
+++ b/crates/dino/src/slab/id.rs
@@ -68,8 +68,8 @@
         })
     }
 
-    pub(crate) fn new_unchecked(raw: u32) -> Self {
-        Self(NonZeroUsize::new(raw as usize).expect("raw is zero"))
+    pub(crate) fn new_unchecked(raw: usize) -> Self {
+        Self(NonZeroUsize::new(raw).expect("raw is zero"))
     }
 
     #[inline]
@@ -88,6 +88,12 @@
 
     #[inline]
     #[must_use]
+    pub(crate) const fn into_usize(self) -> usize {
+        self.0.get()
+    }
+
+    #[inline]
+    #[must_use]
     #[allow(clippy::cast_possible_truncation)]
     pub(crate) fn generation(self) -> Generation {
         Generation(
diff --git a/crates/dino/src/slab/mod.rs b/crates/dino/src/slab/mod.rs
index 44eee1d..7a7e6b7 100644
--- a/crates/dino/src/slab/mod.rs
+++ b/crates/dino/src/slab/mod.rs
@@ -8,8 +8,7 @@
 use alloc::{vec, vec::Vec};
 use core::{fmt::Debug, hash::Hash, marker::PhantomData, ptr};
 
-use numi::borrow::Moo;
-use petgraph_core::id::IndexMapper;
+use petgraph_core::storage::sequential::GraphIdBijection;
 
 use crate::slab::entry::{Entry, State};
 pub(crate) use crate::slab::{generation::Generation, id::EntryId, key::Key};
@@ -428,7 +427,7 @@
     }
 }
 
-impl<K> IndexMapper<K> for SlabIndexMapper<'_, K>
+impl<K> GraphIdBijection<K> for SlabIndexMapper<'_, K>
 where
     K: Key,
 {
diff --git a/crates/dino/src/slab/secondary.rs b/crates/dino/src/slab/secondary.rs
index 23d4e86..cdbe91b 100644
--- a/crates/dino/src/slab/secondary.rs
+++ b/crates/dino/src/slab/secondary.rs
@@ -2,64 +2,65 @@
 use core::iter;
 
 use bitvec::{boxed::BitBox, prelude::BitVec};
-use numi::borrow::Moo;
-use petgraph_core::id::{AttributeMapper, BooleanMapper};
+use petgraph_core::storage::auxiliary::{BooleanGraphStorage, SecondaryGraphStorage};
 
 use crate::slab::{EntryId, Generation, Key, Slab};
 
-pub struct SlabBooleanMapper<'a> {
+pub struct SlabBooleanStorage<'a, K> {
     flags: BitBox,
-    _marker: core::marker::PhantomData<&'a ()>,
+
+    _slab: core::marker::PhantomData<&'a ()>,
+    _key: core::marker::PhantomData<fn() -> *const K>,
 }
 
-impl<'a> SlabBooleanMapper<'a> {
-    pub(crate) fn new<K, V>(slab: &'a Slab<K, V>) -> Self
-    where
-        K: Key,
-    {
+impl<'a, K> SlabBooleanStorage<'a, K>
+where
+    K: Key,
+{
+    pub(crate) fn new<V>(slab: &'a Slab<K, V>) -> Self {
         let length = slab.total_len();
 
         Self {
             flags: BitVec::repeat(false, length).into_boxed_bitslice(),
-            _marker: core::marker::PhantomData,
+
+            _slab: core::marker::PhantomData,
+            _key: core::marker::PhantomData,
         }
     }
 }
 
-impl<Id> BooleanMapper<Id> for SlabBooleanMapper<'_>
+impl<K> BooleanGraphStorage<K> for SlabBooleanStorage<'_, K>
 where
-    Id: Key,
+    K: Key,
 {
     #[inline]
-    fn get(&self, id: Id) -> Option<bool> {
+    fn get(&self, id: K) -> Option<bool> {
         let index = id.into_id().index();
 
         self.flags.get(index).map(|bit| *bit)
     }
 
     #[inline]
-    fn index(&self, id: Id) -> bool {
-        let index = id.into_id().index();
-
-        self.flags[index]
-    }
-
-    #[inline]
-    fn set(&mut self, id: Id, flag: bool) -> Option<bool> {
+    fn set(&mut self, id: K, flag: bool) -> Option<bool> {
         let index = id.into_id().index();
 
         let value = self.flags.replace(index, flag);
 
         Some(value)
     }
+
+    #[inline]
+    fn fill(&mut self, flag: bool) {
+        self.flags.fill(flag);
+    }
 }
 
-pub struct SlabAttributeStorageIter<'a, K, T> {
+pub struct SlabSecondaryStorageIter<'a, K, T> {
     iter: iter::Enumerate<core::slice::Iter<'a, Option<(Generation, T)>>>,
     _marker: core::marker::PhantomData<&'a K>,
 }
 
-impl<'a, K, T> Iterator for SlabAttributeStorageIter<'a, K, T>
+impl<'a, K, T> Iterator for SlabSecondaryStorageIter<'a, K, T>
 where
     K: Key,
 {
@@ -79,7 +80,7 @@
     }
 }
 
-pub struct SlabAttributeMapper<'a, K, T> {
+pub struct SlabSecondaryStorage<'a, K, T> {
     // generation is needed for iter
     items: Vec<Option<(Generation, T)>>,
 
@@ -87,7 +88,7 @@
     _key: core::marker::PhantomData<fn() -> *const K>,
 }
 
-impl<'a, K, T> SlabAttributeMapper<'a, K, T> {
+impl<'a, K, T> SlabSecondaryStorage<'a, K, T> {
     pub(crate) fn new<V>(slab: &'a Slab<K, V>) -> Self
     where
         K: Key,
@@ -103,11 +104,11 @@
     }
 }
 
-impl<K, T> AttributeMapper<K, T> for SlabAttributeMapper<'_, K, T>
+impl<K, T> SecondaryGraphStorage<K, T> for SlabSecondaryStorage<'_, K, T>
 where
     K: Key,
 {
-    type Iter<'a> = SlabAttributeStorageIter<'a, K, T> where
+    type Iter<'a> = SlabSecondaryStorageIter<'a, K, T> where
         K: 'a,
         T: 'a,
         Self: 'a,;
@@ -150,7 +151,7 @@
     }
 
     fn iter(&self) -> Self::Iter<'_> {
-        SlabAttributeStorageIter {
+        SlabSecondaryStorageIter {
             iter: self.items.iter().enumerate(),
             _marker: core::marker::PhantomData,
         }
diff --git a/crates/dino/src/tests.rs b/crates/dino/src/tests.rs
index b042474..a03db3a 100644
--- a/crates/dino/src/tests.rs
+++ b/crates/dino/src/tests.rs
@@ -2,13 +2,12 @@
 
 use hashbrown::HashSet;
 use petgraph_core::{
-    attributes::NoValue,
-    edge::{marker::Directed, DetachedEdge, Direction},
-    node::{DetachedNode, Node, NodeMut},
+    edge::{marker::Directed, DetachedEdge, Direction, EdgeId},
+    node::{DetachedNode, Node, NodeId, NodeMut},
     storage::GraphStorage,
 };
 
-use crate::{DinoGraph, DinoStorage, EdgeId, NodeId};
+use crate::{DinoGraph, DinoStorage};
 
 // TODO: rework tests to be more encompassing and use test utils!
 
@@ -43,9 +42,9 @@
     let mut graph = DinoGraph::<(), u8, Directed>::new();
 
     let node = graph.try_insert_node(()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    let edge = graph.try_insert_edge(2u8, &node, &node).unwrap();
+    let edge = graph.try_insert_edge(2u8, node, node).unwrap();
 
     assert_eq!(edge.weight(), &2u8);
 
@@ -60,17 +59,17 @@
 fn next_node_id_pure() {
     let mut storage = DinoStorage::<(), (), Directed>::new();
 
-    let a = storage.next_node_id(NoValue::new());
-    let b = storage.next_node_id(NoValue::new());
+    let a = storage.next_node_id();
+    let b = storage.next_node_id();
 
     assert_eq!(a, b);
 
     let node = storage.insert_node(a, ()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
     assert_eq!(node, a);
 
-    let c = storage.next_node_id(NoValue::new());
+    let c = storage.next_node_id();
 
     assert_ne!(a, c);
 }
@@ -79,22 +78,20 @@
 fn next_edge_id_pure() {
     let mut storage = DinoStorage::<(), (), Directed>::new();
 
-    let node = storage
-        .insert_node(storage.next_node_id(NoValue::new()), ())
-        .unwrap();
-    let node = *node.id();
+    let node = storage.insert_node(storage.next_node_id(), ()).unwrap();
+    let node = node.id();
 
-    let a = storage.next_edge_id(NoValue::new());
-    let b = storage.next_edge_id(NoValue::new());
+    let a = storage.next_edge_id();
+    let b = storage.next_edge_id();
 
     assert_eq!(a, b);
 
-    let edge = storage.insert_edge(a, (), &node, &node).unwrap();
-    let edge = *edge.id();
+    let edge = storage.insert_edge(a, (), node, node).unwrap();
+    let edge = edge.id();
 
     assert_eq!(edge, a);
 
-    let c = storage.next_edge_id(NoValue::new());
+    let c = storage.next_edge_id();
 
     assert_ne!(a, c);
 }
@@ -104,9 +101,9 @@
     let mut graph = DinoGraph::<u8, (), Directed>::new();
 
     let node = graph.try_insert_node(2u8).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    assert_eq!(graph.remove_node(&node), Some(DetachedNode::new(node, 2u8)));
+    assert_eq!(graph.remove_node(node), Some(DetachedNode::new(node, 2u8)));
 
     assert_eq!(graph.num_nodes(), 0);
     assert_eq!(graph.num_edges(), 0);
@@ -120,13 +117,13 @@
     let mut graph = DinoGraph::<(), u8, Directed>::new();
 
     let node = graph.try_insert_node(()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    let edge = graph.try_insert_edge(2u8, &node, &node).unwrap();
-    let edge = *edge.id();
+    let edge = graph.try_insert_edge(2u8, node, node).unwrap();
+    let edge = edge.id();
 
     assert_eq!(
-        graph.remove_edge(&edge),
+        graph.remove_edge(edge),
         Some(DetachedEdge::new(edge, 2u8, node, node))
     );
 
@@ -136,32 +133,28 @@
     assert_eq!(graph.nodes().count(), 1);
     assert_eq!(graph.edges().count(), 0);
 
-    assert_eq!(graph.connections(&node).count(), 0);
-    assert_eq!(graph.neighbours(&node).count(), 0);
+    assert_eq!(graph.connections(node).count(), 0);
+    assert_eq!(graph.neighbours(node).count(), 0);
 
     assert_eq!(
         graph
-            .connections_directed(&node, Direction::Incoming)
+            .connections_directed(node, Direction::Incoming)
             .count(),
         0
     );
     assert_eq!(
         graph
-            .connections_directed(&node, Direction::Outgoing)
+            .connections_directed(node, Direction::Outgoing)
             .count(),
         0
     );
 
     assert_eq!(
-        graph
-            .neighbours_directed(&node, Direction::Incoming)
-            .count(),
+        graph.neighbours_directed(node, Direction::Incoming).count(),
         0
     );
     assert_eq!(
-        graph
-            .neighbours_directed(&node, Direction::Outgoing)
-            .count(),
+        graph.neighbours_directed(node, Direction::Outgoing).count(),
         0
     );
 }
@@ -171,9 +164,9 @@
     let mut graph = DinoGraph::<u8, u8, Directed>::new();
 
     let node = graph.try_insert_node(2u8).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    graph.try_insert_edge(2u8, &node, &node).unwrap();
+    graph.try_insert_edge(2u8, node, node).unwrap();
 
     graph.clear();
 
@@ -189,11 +182,11 @@
     let mut graph = DinoGraph::<u8, (), Directed>::new();
 
     let node = graph.try_insert_node(2u8).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
     assert_eq!(
-        graph.node(&node),
-        Some(Node::new(graph.storage(), &node, &2u8))
+        graph.node(node),
+        Some(Node::new(graph.storage(), node, &2u8))
     );
 }
 
@@ -202,17 +195,17 @@
     let mut graph = DinoGraph::<u8, (), Directed>::new();
 
     let node = graph.try_insert_node(2u8).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    assert_eq!(graph.node_mut(&node), Some(NodeMut::new(&node, &mut 2u8)));
+    assert_eq!(graph.node_mut(node), Some(NodeMut::new(node, &mut 2u8)));
 
-    let mut node = graph.node_mut(&node).unwrap();
+    let mut node = graph.node_mut(node).unwrap();
     *node.weight_mut() = 3u8;
-    let node = *node.id();
+    let node = node.id();
 
     assert_eq!(
-        graph.node(&node),
-        Some(Node::new(graph.storage(), &node, &3u8))
+        graph.node(node),
+        Some(Node::new(graph.storage(), node, &3u8))
     );
 }
 
@@ -221,9 +214,9 @@
     let mut graph = DinoGraph::<u8, (), Directed>::new();
 
     let node = graph.try_insert_node(2u8).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    assert!(graph.storage().contains_node(&node));
+    assert!(graph.storage().contains_node(node));
 }
 
 #[test]
@@ -231,19 +224,19 @@
     let mut graph = DinoGraph::<(), u8, Directed>::new();
 
     let node = graph.try_insert_node(()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    let edge = graph.try_insert_edge(2u8, &node, &node).unwrap();
-    let edge = *edge.id();
+    let edge = graph.try_insert_edge(2u8, node, node).unwrap();
+    let edge = edge.id();
 
     assert_eq!(
-        graph.edge(&edge),
+        graph.edge(edge),
         Some(petgraph_core::edge::Edge::new(
             graph.storage(),
-            &edge,
+            edge,
             &2u8,
-            &node,
-            &node
+            node,
+            node
         ))
     );
 }
@@ -253,30 +246,30 @@
     let mut graph = DinoGraph::<(), u8, Directed>::new();
 
     let node = graph.try_insert_node(()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    let edge = graph.try_insert_edge(2u8, &node, &node).unwrap();
-    let edge = *edge.id();
+    let edge = graph.try_insert_edge(2u8, node, node).unwrap();
+    let edge = edge.id();
 
     assert_eq!(
-        graph.edge_mut(&edge),
+        graph.edge_mut(edge),
         Some(petgraph_core::edge::EdgeMut::new(
-            &edge, &mut 2u8, &node, &node
+            edge, &mut 2u8, node, node
         ))
     );
 
-    let mut edge = graph.edge_mut(&edge).unwrap();
+    let mut edge = graph.edge_mut(edge).unwrap();
     *edge.weight_mut() = 3u8;
-    let edge = *edge.id();
+    let edge = edge.id();
 
     assert_eq!(
-        graph.edge(&edge),
+        graph.edge(edge),
         Some(petgraph_core::edge::Edge::new(
             graph.storage(),
-            &edge,
+            edge,
             &3u8,
-            &node,
-            &node
+            node,
+            node
         ))
     );
 }
@@ -286,12 +279,12 @@
     let mut graph = DinoGraph::<(), u8, Directed>::new();
 
     let node = graph.try_insert_node(()).unwrap();
-    let node = *node.id();
+    let node = node.id();
 
-    let edge = graph.try_insert_edge(2u8, &node, &node).unwrap();
-    let edge = *edge.id();
+    let edge = graph.try_insert_edge(2u8, node, node).unwrap();
+    let edge = edge.id();
 
-    assert!(graph.storage().contains_edge(&edge));
+    assert!(graph.storage().contains_edge(edge));
 }
 
 struct SimpleGraph {
@@ -314,31 +307,31 @@
         let mut graph = DinoGraph::new();
 
         let a = graph.try_insert_node(1u8).unwrap();
-        let a = *a.id();
+        let a = a.id();
 
         let b = graph.try_insert_node(2u8).unwrap();
-        let b = *b.id();
+        let b = b.id();
 
         let c = graph.try_insert_node(3u8).unwrap();
-        let c = *c.id();
+        let c = c.id();
 
         let d = graph.try_insert_node(8u8).unwrap();
-        let d = *d.id();
+        let d = d.id();
 
-        let ab = graph.try_insert_edge(4u8, &a, &b).unwrap();
-        let ab = *ab.id();
+        let ab = graph.try_insert_edge(4u8, a, b).unwrap();
+        let ab = ab.id();
 
-        let ba = graph.try_insert_edge(5u8, &b, &a).unwrap();
-        let ba = *ba.id();
+        let ba = graph.try_insert_edge(5u8, b, a).unwrap();
+        let ba = ba.id();
 
-        let bc = graph.try_insert_edge(6u8, &b, &c).unwrap();
-        let bc = *bc.id();
+        let bc = graph.try_insert_edge(6u8, b, c).unwrap();
+        let bc = bc.id();
 
-        let ac = graph.try_insert_edge(7u8, &a, &c).unwrap();
-        let ac = *ac.id();
+        let ac = graph.try_insert_edge(7u8, a, c).unwrap();
+        let ac = ac.id();
 
-        let ca = graph.try_insert_edge(8u8, &c, &a).unwrap();
-        let ca = *ca.id();
+        let ca = graph.try_insert_edge(8u8, c, a).unwrap();
+        let ca = ca.id();
 
         Self {
             graph,
@@ -368,7 +361,7 @@
 
     assert_eq!(
         graph
-            .edges_between(&a, &b)
+            .edges_between(a, b)
             .map(petgraph_core::edge::Edge::detach)
             .collect::<HashSet<_>>(),
         [
@@ -391,13 +384,13 @@
         ..
     } = SimpleGraph::create();
 
-    for mut edge in graph.edges_between_mut(&a, &b) {
+    for mut edge in graph.edges_between_mut(a, b) {
         *edge.weight_mut() += 1;
     }
 
     assert_eq!(
         graph
-            .edges_between(&a, &b)
+            .edges_between(a, b)
             .map(petgraph_core::edge::Edge::detach)
             .collect::<HashSet<_>>(),
         [
@@ -425,7 +418,7 @@
 
     assert_eq!(
         graph
-            .connections(&a)
+            .connections(a)
             .map(petgraph_core::edge::Edge::detach)
             .collect::<HashSet<_>>(),
         [
@@ -453,13 +446,13 @@
         bc,
         ..
     } = SimpleGraph::create();
-    for mut connection in graph.connections_mut(&a) {
+    for mut connection in graph.connections_mut(a) {
         *connection.weight_mut() += 1;
     }
 
     assert_eq!(
         graph
-            .connections(&a)
+            .connections(a)
             .map(petgraph_core::edge::Edge::detach)
             .collect::<HashSet<_>>(),
         [
@@ -473,13 +466,13 @@
     );
 
     assert_eq!(
-        graph.edge(&bc),
+        graph.edge(bc),
         Some(petgraph_core::edge::Edge::new(
             graph.storage(),
-            &bc,
+            bc,
             &6u8,
-            &b,
-            &c
+            b,
+            c
         ))
     );
 }
@@ -492,7 +485,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&a)
+            .neighbours(a)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(b, 2u8), DetachedNode::new(c, 3u8)]
@@ -502,7 +495,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&b)
+            .neighbours(b)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(a, 1u8), DetachedNode::new(c, 3u8)]
@@ -512,7 +505,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&c)
+            .neighbours(c)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(a, 1u8), DetachedNode::new(b, 2u8)]
@@ -522,7 +515,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&d)
+            .neighbours(d)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         HashSet::new()
@@ -540,13 +533,13 @@
         ..
     } = SimpleGraph::create();
 
-    for mut neighbour in graph.neighbours_mut(&a) {
+    for mut neighbour in graph.neighbours_mut(a) {
         *neighbour.weight_mut() += 1;
     }
 
     assert_eq!(
         graph
-            .neighbours(&a)
+            .neighbours(a)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(b, 3u8), DetachedNode::new(c, 4u8)]
@@ -556,7 +549,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&b)
+            .neighbours(b)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(a, 1u8), DetachedNode::new(c, 4u8)]
@@ -566,7 +559,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&c)
+            .neighbours(c)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         [DetachedNode::new(a, 1u8), DetachedNode::new(b, 3u8)]
@@ -576,7 +569,7 @@
 
     assert_eq!(
         graph
-            .neighbours(&d)
+            .neighbours(d)
             .map(Node::detach)
             .collect::<HashSet<_>>(),
         HashSet::new()
@@ -619,11 +612,11 @@
         once(DetachedNode::new(d, 9u8)).collect::<HashSet<_>>()
     );
 
-    assert_eq!(graph.node(&a), Some(Node::new(graph.storage(), &a, &1u8)));
+    assert_eq!(graph.node(a), Some(Node::new(graph.storage(), a, &1u8)));
 
-    assert_eq!(graph.node(&b), Some(Node::new(graph.storage(), &b, &2u8)));
+    assert_eq!(graph.node(b), Some(Node::new(graph.storage(), b, &2u8)));
 
-    assert_eq!(graph.node(&c), Some(Node::new(graph.storage(), &c, &3u8)));
+    assert_eq!(graph.node(c), Some(Node::new(graph.storage(), c, &3u8)));
 }
 
 #[test]
diff --git a/crates/graphmap/CHANGELOG.md b/crates/entry/CHANGELOG.md
similarity index 100%
rename from crates/graphmap/CHANGELOG.md
rename to crates/entry/CHANGELOG.md
diff --git a/crates/entry/Cargo.toml b/crates/entry/Cargo.toml
new file mode 100644
index 0000000..ef41ae9
--- /dev/null
+++ b/crates/entry/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "petgraph-entry"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[dependencies]
+petgraph-core = { workspace = true }
+petgraph-dino = { workspace = true }
+
+error-stack = { workspace = true }
+hashbrown = "0.14.3"
+
+[features]
diff --git a/crates/entry/src/auxiliary.rs b/crates/entry/src/auxiliary.rs
new file mode 100644
index 0000000..9ddd8d4
--- /dev/null
+++ b/crates/entry/src/auxiliary.rs
@@ -0,0 +1,36 @@
+use core::hash::Hash;
+
+use petgraph_core::{
+    storage::{auxiliary::Hints, AuxiliaryGraphStorage},
+    GraphDirectionality,
+};
+
+use crate::{Backend, EntryStorage};
+
+impl<NK, NV, EK, EV, D> AuxiliaryGraphStorage for EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    type BooleanEdgeStorage<'a> = <Backend<NK, NV, EK, EV, D> as AuxiliaryGraphStorage>::BooleanEdgeStorage<'a> where Self: 'a;
+    type BooleanNodeStorage<'a> = <Backend<NK, NV, EK, EV, D> as AuxiliaryGraphStorage>::BooleanNodeStorage<'a> where Self: 'a;
+    type SecondaryEdgeStorage<'a, V> = <Backend<NK, NV, EK, EV, D> as AuxiliaryGraphStorage>::SecondaryEdgeStorage<'a, V> where Self: 'a;
+    type SecondaryNodeStorage<'a, V> = <Backend<NK, NV, EK, EV, D> as AuxiliaryGraphStorage>::SecondaryNodeStorage<'a, V> where Self: 'a;
+
+    fn secondary_node_storage<V>(&self, hints: Hints) -> Self::SecondaryNodeStorage<'_, V> {
+        self.inner.secondary_node_storage(hints)
+    }
+
+    fn secondary_edge_storage<V>(&self, hints: Hints) -> Self::SecondaryEdgeStorage<'_, V> {
+        self.inner.secondary_edge_storage(hints)
+    }
+
+    fn boolean_node_storage(&self, hints: Hints) -> Self::BooleanNodeStorage<'_> {
+        self.inner.boolean_node_storage(hints)
+    }
+
+    fn boolean_edge_storage(&self, hints: Hints) -> Self::BooleanEdgeStorage<'_> {
+        self.inner.boolean_edge_storage(hints)
+    }
+}
diff --git a/crates/entry/src/directed.rs b/crates/entry/src/directed.rs
new file mode 100644
index 0000000..a4f8e9b
--- /dev/null
+++ b/crates/entry/src/directed.rs
@@ -0,0 +1,79 @@
+use core::hash::Hash;
+
+use petgraph_core::{
+    edge::{marker::Directed, Direction},
+    node::NodeId,
+    DirectedGraphStorage, Edge, EdgeMut, Node, NodeMut,
+};
+
+use crate::EntryStorage;
+
+impl<NK, NV, EK, EV> DirectedGraphStorage for EntryStorage<NK, NV, EK, EV, Directed>
+where
+    NK: Hash,
+    EK: Hash,
+{
+    fn directed_edges_between(
+        &self,
+        source: NodeId,
+        target: NodeId,
+    ) -> impl Iterator<Item = Edge<'_, Self>> {
+        self.inner
+            .directed_edges_between(source, target)
+            .map(|edge| edge.change_storage_unchecked(self))
+    }
+
+    fn directed_edges_between_mut(
+        &mut self,
+        source: NodeId,
+        target: NodeId,
+    ) -> impl Iterator<Item = EdgeMut<'_, Self>> {
+        self.inner
+            .directed_edges_between_mut(source, target)
+            .map(EdgeMut::change_storage_unchecked)
+    }
+
+    fn node_directed_connections(
+        &self,
+        id: NodeId,
+        direction: Direction,
+    ) -> impl Iterator<Item = Edge<'_, Self>> {
+        self.inner
+            .node_directed_connections(id, direction)
+            .map(|edge| edge.change_storage_unchecked(self))
+    }
+
+    fn node_directed_connections_mut(
+        &mut self,
+        id: NodeId,
+        direction: Direction,
+    ) -> impl Iterator<Item = EdgeMut<'_, Self>> {
+        self.inner
+            .node_directed_connections_mut(id, direction)
+            .map(EdgeMut::change_storage_unchecked)
+    }
+
+    fn node_directed_degree(&self, id: NodeId, direction: Direction) -> usize {
+        self.inner.node_directed_degree(id, direction)
+    }
+
+    fn node_directed_neighbours(
+        &self,
+        id: NodeId,
+        direction: Direction,
+    ) -> impl Iterator<Item = Node<'_, Self>> {
+        self.inner
+            .node_directed_neighbours(id, direction)
+            .map(|node| node.change_storage_unchecked(self))
+    }
+
+    fn node_directed_neighbours_mut(
+        &mut self,
+        id: NodeId,
+        direction: Direction,
+    ) -> impl Iterator<Item = NodeMut<'_, Self>> {
+        self.inner
+            .node_directed_neighbours_mut(id, direction)
+            .map(NodeMut::change_storage_unchecked)
+    }
+}
diff --git a/crates/entry/src/error.rs b/crates/entry/src/error.rs
new file mode 100644
index 0000000..d36d1b0
--- /dev/null
+++ b/crates/entry/src/error.rs
@@ -0,0 +1,35 @@
+use core::{fmt, fmt::Display};
+
+use error_stack::Context;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum EntryError {
+    /// An underlying backend error occurred.
+    ///
+    /// Refer to the underlying attached backend error for more information on what went wrong.
+    Backend,
+    /// The node you are trying to insert already exists.
+    ///
+    /// This means that the [`Entry::key`] you are trying to insert already exists in the graph.
+    ///
+    /// [`Entry::key`]: crate::Entry::key
+    NodeAlreadyExists,
+    /// The edge you are trying to insert already exists.
+    ///
+    /// This means that the [`Entry::key`] you are trying to insert already exists in the graph.
+    ///
+    /// [`Entry::key`]: crate::Entry::key
+    EdgeAlreadyExists,
+}
+
+impl Display for EntryError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Backend => write!(f, "Backend error"),
+            Self::NodeAlreadyExists => write!(f, "Node already exists"),
+            Self::EdgeAlreadyExists => write!(f, "Edge already exists"),
+        }
+    }
+}
+
+impl Context for EntryError {}
diff --git a/crates/entry/src/hash.rs b/crates/entry/src/hash.rs
new file mode 100644
index 0000000..859e6cb
--- /dev/null
+++ b/crates/entry/src/hash.rs
@@ -0,0 +1,47 @@
+use core::hash::{BuildHasher, Hash, Hasher};
+
+pub(crate) struct ValueHash<T> {
+    value: u64,
+    _phantom: core::marker::PhantomData<T>,
+}
+
+impl<T> ValueHash<T> {
+    pub(crate) fn new<S>(hash_builder: &S, value: &T) -> Self
+    where
+        T: Hash,
+        S: BuildHasher,
+    {
+        let hash = {
+            let mut hasher = hash_builder.build_hasher();
+            value.hash(&mut hasher);
+            hasher.finish()
+        };
+
+        Self {
+            value: hash,
+            _phantom: core::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> PartialEq for ValueHash<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.value == other.value
+    }
+}
+
+impl<T> Eq for ValueHash<T> {}
+
+impl<T> Hash for ValueHash<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.value.hash(state);
+    }
+}
+
+impl<T> Clone for ValueHash<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+impl<T> Copy for ValueHash<T> {}
diff --git a/crates/entry/src/lib.rs b/crates/entry/src/lib.rs
new file mode 100644
index 0000000..2342584
--- /dev/null
+++ b/crates/entry/src/lib.rs
@@ -0,0 +1,343 @@
+#![no_std]
+
+mod auxiliary;
+mod directed;
+mod error;
+mod hash;
+mod retain;
+mod reverse;
+mod sequential;
+
+extern crate alloc;
+
+use core::hash::Hash;
+
+use error_stack::{Report, ResultExt};
+use hashbrown::{hash_map::DefaultHashBuilder, HashMap};
+use petgraph_core::{
+    edge::{
+        marker::{Directed, Undirected},
+        EdgeId,
+    },
+    node::NodeId,
+    DetachedEdge, DetachedNode, Edge, EdgeMut, Graph, GraphDirectionality, GraphStorage, Node,
+    NodeMut,
+};
+use petgraph_dino::DinoStorage;
+
+use crate::{error::EntryError, hash::ValueHash};
+
+pub struct Entry<K, V> {
+    key: K,
+    pub value: V,
+}
+
+impl<K, V> Entry<K, V> {
+    #[must_use]
+    pub const fn new(key: K, value: V) -> Self {
+        Self { key, value }
+    }
+
+    pub const fn key(&self) -> &K {
+        &self.key
+    }
+}
+
+pub type EntryGraph<NK, NV, EK, EV, D> = Graph<EntryStorage<NK, NV, EK, EV, D>>;
+pub type UnEntryGraph<NK, NV, EK, EV> =
+    Graph<DinoStorage<Entry<NK, NV>, Entry<EK, EV>, Undirected>>;
+
+pub type DiEntryGraph<NK, NV, EK, EV> = Graph<DinoStorage<Entry<NK, NV>, Entry<EK, EV>, Directed>>;
+
+type Backend<NK, NV, EK, EV, D> = DinoStorage<Entry<NK, NV>, Entry<EK, EV>, D>;
+
+// TODO: better name
+// TODO: reduce generics
+pub struct EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    inner: Backend<NK, NV, EK, EV, D>,
+
+    nodes: HashMap<ValueHash<NK>, NodeId>,
+    edges: HashMap<ValueHash<EK>, EdgeId>,
+
+    hasher: DefaultHashBuilder,
+}
+
+impl<NK, NV, EK, EV, D> GraphStorage for EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    type EdgeWeight = Entry<EK, EV>;
+    type Error = EntryError;
+    type NodeWeight = Entry<NK, NV>;
+
+    fn with_capacity(node_capacity: Option<usize>, edge_capacity: Option<usize>) -> Self {
+        Self {
+            inner: DinoStorage::with_capacity(node_capacity, edge_capacity),
+            nodes: HashMap::with_capacity(node_capacity.unwrap_or(0)),
+            edges: HashMap::with_capacity(edge_capacity.unwrap_or(0)),
+            hasher: DefaultHashBuilder::default(),
+        }
+    }
+
+    fn from_parts(
+        nodes: impl IntoIterator<Item = DetachedNode<Self::NodeWeight>>,
+        edges: impl IntoIterator<Item = DetachedEdge<Self::EdgeWeight>>,
+    ) -> error_stack::Result<Self, Self::Error> {
+        todo!()
+    }
+
+    fn into_parts(
+        self,
+    ) -> (
+        impl Iterator<Item = DetachedNode<Self::NodeWeight>>,
+        impl Iterator<Item = DetachedEdge<Self::EdgeWeight>>,
+    ) {
+        self.inner.into_parts()
+    }
+
+    fn num_nodes(&self) -> usize {
+        self.inner.num_nodes()
+    }
+
+    fn num_edges(&self) -> usize {
+        self.inner.num_edges()
+    }
+
+    fn next_node_id(&self) -> NodeId {
+        self.inner.next_node_id()
+    }
+
+    fn insert_node(
+        &mut self,
+        id: NodeId,
+        weight: Self::NodeWeight,
+    ) -> error_stack::Result<NodeMut<Self>, Self::Error> {
+        let hash = ValueHash::new(&self.hasher, &weight.key);
+
+        if self.nodes.contains_key(&hash) {
+            return Err(Report::new(EntryError::NodeAlreadyExists));
+        }
+
+        let node = self
+            .inner
+            .insert_node(id, weight)
+            .change_context(EntryError::Backend)?;
+        self.nodes.insert(hash, id);
+
+        Ok(node.change_storage_unchecked())
+    }
+
+    fn next_edge_id(&self) -> EdgeId {
+        self.inner.next_edge_id()
+    }
+
+    fn insert_edge(
+        &mut self,
+        id: EdgeId,
+        weight: Self::EdgeWeight,
+        u: NodeId,
+        v: NodeId,
+    ) -> error_stack::Result<EdgeMut<Self>, Self::Error> {
+        let hash = ValueHash::new(&self.hasher, &weight.key);
+
+        if self.edges.contains_key(&hash) {
+            return Err(Report::new(EntryError::EdgeAlreadyExists));
+        }
+
+        let edge = self
+            .inner
+            .insert_edge(id, weight, u, v)
+            .change_context(EntryError::Backend)?;
+        self.edges.insert(hash, id);
+
+        Ok(edge.change_storage_unchecked())
+    }
+
+    fn remove_node(&mut self, id: NodeId) -> Option<DetachedNode<Self::NodeWeight>> {
+        let node = self.inner.remove_node(id)?;
+        let hash = ValueHash::new(&self.hasher, &node.weight.key);
+        self.nodes.remove(&hash);
+
+        Some(node)
+    }
+
+    fn remove_edge(&mut self, id: EdgeId) -> Option<DetachedEdge<Self::EdgeWeight>> {
+        let edge = self.inner.remove_edge(id)?;
+        let hash = ValueHash::new(&self.hasher, &edge.weight.key);
+        self.edges.remove(&hash);
+
+        Some(edge)
+    }
+
+    fn clear(&mut self) {
+        self.inner.clear();
+        self.nodes.clear();
+        self.edges.clear();
+    }
+
+    fn node(&self, id: NodeId) -> Option<Node<Self>> {
+        let node = self.inner.node(id)?;
+        Some(node.change_storage_unchecked(self))
+    }
+
+    fn node_mut(&mut self, id: NodeId) -> Option<NodeMut<Self>> {
+        let node = self.inner.node_mut(id)?;
+        Some(node.change_storage_unchecked())
+    }
+
+    fn contains_node(&self, id: NodeId) -> bool {
+        self.inner.contains_node(id)
+    }
+
+    fn edge(&self, id: EdgeId) -> Option<Edge<Self>> {
+        let edge = self.inner.edge(id)?;
+
+        Some(edge.change_storage_unchecked(self))
+    }
+
+    fn edge_mut(&mut self, id: EdgeId) -> Option<EdgeMut<Self>> {
+        let edge = self.inner.edge_mut(id)?;
+
+        Some(edge.change_storage_unchecked())
+    }
+
+    fn contains_edge(&self, id: EdgeId) -> bool {
+        self.inner.contains_edge(id)
+    }
+
+    fn edges_between(&self, u: NodeId, v: NodeId) -> impl Iterator<Item = Edge<'_, Self>> {
+        self.inner
+            .edges_between(u, v)
+            .map(|edge| edge.change_storage_unchecked(self))
+    }
+
+    fn edges_between_mut(
+        &mut self,
+        u: NodeId,
+        v: NodeId,
+    ) -> impl Iterator<Item = EdgeMut<'_, Self>> {
+        self.inner
+            .edges_between_mut(u, v)
+            .map(EdgeMut::change_storage_unchecked)
+    }
+
+    fn node_connections(&self, id: NodeId) -> impl Iterator<Item = Edge<'_, Self>> {
+        self.inner
+            .node_connections(id)
+            .map(|edge| edge.change_storage_unchecked(self))
+    }
+
+    fn node_connections_mut(&mut self, id: NodeId) -> impl Iterator<Item = EdgeMut<'_, Self>> {
+        self.inner
+            .node_connections_mut(id)
+            .map(EdgeMut::change_storage_unchecked)
+    }
+
+    fn node_degree(&self, id: NodeId) -> usize {
+        self.inner.node_degree(id)
+    }
+
+    fn node_neighbours(&self, id: NodeId) -> impl Iterator<Item = Node<'_, Self>> {
+        self.inner
+            .node_neighbours(id)
+            .map(|node| node.change_storage_unchecked(self))
+    }
+
+    fn node_neighbours_mut(&mut self, id: NodeId) -> impl Iterator<Item = NodeMut<'_, Self>> {
+        self.inner
+            .node_neighbours_mut(id)
+            .map(NodeMut::change_storage_unchecked)
+    }
+
+    fn isolated_nodes(&self) -> impl Iterator<Item = Node<Self>> {
+        self.inner
+            .isolated_nodes()
+            .map(|node| node.change_storage_unchecked(self))
+    }
+
+    fn isolated_nodes_mut(&mut self) -> impl Iterator<Item = NodeMut<Self>> {
+        self.inner
+            .isolated_nodes_mut()
+            .map(NodeMut::change_storage_unchecked)
+    }
+
+    fn nodes(&self) -> impl Iterator<Item = Node<Self>> {
+        self.inner
+            .nodes()
+            .map(|node| node.change_storage_unchecked(self))
+    }
+
+    fn nodes_mut(&mut self) -> impl Iterator<Item = NodeMut<Self>> {
+        self.inner
+            .nodes_mut()
+            .map(NodeMut::change_storage_unchecked)
+    }
+
+    fn edges(&self) -> impl Iterator<Item = Edge<Self>> {
+        self.inner
+            .edges()
+            .map(|edge| edge.change_storage_unchecked(self))
+    }
+
+    fn edges_mut(&mut self) -> impl Iterator<Item = EdgeMut<Self>> {
+        self.inner
+            .edges_mut()
+            .map(EdgeMut::change_storage_unchecked)
+    }
+
+    fn reserve(&mut self, additional_nodes: usize, additional_edges: usize) {
+        self.inner.reserve(additional_nodes, additional_edges);
+        self.nodes.reserve(additional_nodes);
+        self.edges.reserve(additional_edges);
+    }
+
+    fn reserve_nodes(&mut self, additional: usize) {
+        self.inner.reserve_nodes(additional);
+        self.nodes.reserve(additional);
+    }
+
+    fn reserve_edges(&mut self, additional: usize) {
+        self.inner.reserve_edges(additional);
+        self.edges.reserve(additional);
+    }
+
+    fn reserve_exact(&mut self, additional_nodes: usize, additional_edges: usize) {
+        self.inner.reserve_exact(additional_nodes, additional_edges);
+
+        self.nodes.reserve(additional_nodes);
+        self.edges.reserve(additional_edges);
+    }
+
+    fn reserve_exact_nodes(&mut self, additional: usize) {
+        self.inner.reserve_exact_nodes(additional);
+        self.nodes.reserve(additional);
+    }
+
+    fn reserve_exact_edges(&mut self, additional: usize) {
+        self.inner.reserve_exact_edges(additional);
+        self.edges.reserve(additional);
+    }
+
+    fn shrink_to_fit(&mut self) {
+        self.inner.shrink_to_fit();
+        self.nodes.shrink_to_fit();
+        self.edges.shrink_to_fit();
+    }
+
+    fn shrink_to_fit_nodes(&mut self) {
+        self.inner.shrink_to_fit_nodes();
+        self.nodes.shrink_to_fit();
+    }
+
+    fn shrink_to_fit_edges(&mut self) {
+        self.inner.shrink_to_fit_edges();
+        self.edges.shrink_to_fit();
+    }
+}
diff --git a/crates/entry/src/retain.rs b/crates/entry/src/retain.rs
new file mode 100644
index 0000000..aae7492
--- /dev/null
+++ b/crates/entry/src/retain.rs
@@ -0,0 +1,48 @@
+use alloc::vec;
+use core::hash::Hash;
+
+use petgraph_core::{
+    storage::RetainableGraphStorage, EdgeMut, GraphDirectionality, GraphStorage, NodeMut,
+};
+
+use crate::EntryStorage;
+
+impl<NK, NV, EK, EV, D> RetainableGraphStorage for EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    fn retain_nodes(&mut self, mut f: impl FnMut(NodeMut<'_, Self>) -> bool) {
+        let mut remove = vec![];
+
+        for node in self.inner.nodes_mut() {
+            let node = node.change_storage_unchecked();
+            let id = node.id();
+
+            if !f(node) {
+                remove.push(id);
+            }
+        }
+
+        for id in remove {
+            self.remove_node(id);
+        }
+    }
+
+    fn retain_edges(&mut self, mut f: impl FnMut(EdgeMut<'_, Self>) -> bool) {
+        let mut remove = vec![];
+        for edge in self.inner.edges_mut() {
+            let edge = edge.change_storage_unchecked();
+            let id = edge.id();
+
+            if !f(edge) {
+                remove.push(id);
+            }
+        }
+
+        for id in remove {
+            self.remove_edge(id);
+        }
+    }
+}
diff --git a/crates/entry/src/reverse.rs b/crates/entry/src/reverse.rs
new file mode 100644
index 0000000..642dee3
--- /dev/null
+++ b/crates/entry/src/reverse.rs
@@ -0,0 +1,57 @@
+use core::hash::Hash;
+
+use petgraph_core::{
+    storage::reverse::ReverseGraphStorage, Edge, EdgeMut, GraphDirectionality, GraphStorage, Node,
+    NodeMut,
+};
+
+use crate::{hash::ValueHash, EntryStorage};
+
+impl<NK, NV, EK, EV, D> ReverseGraphStorage for EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    type EdgeKey = EK;
+    type NodeKey = NK;
+
+    fn contains_node_key(&self, key: &Self::NodeKey) -> bool {
+        let hash = ValueHash::new(&self.hasher, key);
+        self.nodes.contains_key(&hash)
+    }
+
+    fn node_by_key(&self, key: &Self::NodeKey) -> Option<Node<Self>> {
+        let hash = ValueHash::new(&self.hasher, key);
+
+        self.nodes.get(&hash).and_then(|id| self.node(*id))
+    }
+
+    fn node_by_key_mut(&mut self, key: &Self::NodeKey) -> Option<NodeMut<Self>> {
+        let hash = ValueHash::new(&self.hasher, key);
+
+        let &node = self.nodes.get(&hash)?;
+
+        self.node_mut(node)
+    }
+
+    fn contains_edge_key(&self, key: &Self::EdgeKey) -> bool {
+        let hash = ValueHash::new(&self.hasher, key);
+
+        self.edges.contains_key(&hash)
+    }
+
+    fn edge_by_key(&self, key: &Self::EdgeKey) -> Option<Edge<Self>> {
+        let hash = ValueHash::new(&self.hasher, key);
+
+        self.edges.get(&hash).and_then(|id| self.edge(*id))
+    }
+
+    fn edge_by_key_mut(&mut self, key: &Self::EdgeKey) -> Option<EdgeMut<Self>> {
+        let hash = ValueHash::new(&self.hasher, key);
+
+        let &edge = self.edges.get(&hash)?;
+
+        self.edge_mut(edge)
+    }
+}
diff --git a/crates/entry/src/sequential.rs b/crates/entry/src/sequential.rs
new file mode 100644
index 0000000..4c52145
--- /dev/null
+++ b/crates/entry/src/sequential.rs
@@ -0,0 +1,23 @@
+use core::hash::Hash;
+
+use petgraph_core::{storage::SequentialGraphStorage, GraphDirectionality};
+
+use crate::{Backend, EntryStorage};
+
+impl<NK, NV, EK, EV, D> SequentialGraphStorage for EntryStorage<NK, NV, EK, EV, D>
+where
+    D: GraphDirectionality,
+    NK: Hash,
+    EK: Hash,
+{
+    type EdgeIdBijection<'a> = <Backend<NK, NV, EK, EV, D> as SequentialGraphStorage>::EdgeIdBijection<'a> where Self: 'a;
+    type NodeIdBijection<'a> = <Backend<NK, NV, EK, EV, D> as SequentialGraphStorage>::NodeIdBijection<'a> where Self: 'a;
+
+    fn node_id_bijection(&self) -> Self::NodeIdBijection<'_> {
+        self.inner.node_id_bijection()
+    }
+
+    fn edge_id_bijection(&self) -> Self::EdgeIdBijection<'_> {
+        self.inner.edge_id_bijection()
+    }
+}
diff --git a/crates/graphmap/tests/snapshots/test_serde__serialize.snap b/crates/entry/tests/snapshots/test_serde__serialize.snap
similarity index 100%
rename from crates/graphmap/tests/snapshots/test_serde__serialize.snap
rename to crates/entry/tests/snapshots/test_serde__serialize.snap
diff --git a/crates/graphmap/tests/snapshots/test_serde__serialize_struct.snap b/crates/entry/tests/snapshots/test_serde__serialize_struct.snap
similarity index 100%
rename from crates/graphmap/tests/snapshots/test_serde__serialize_struct.snap
rename to crates/entry/tests/snapshots/test_serde__serialize_struct.snap
diff --git a/crates/graphmap/tests/test_directed.rs b/crates/entry/tests/test_directed.BAK
similarity index 92%
rename from crates/graphmap/tests/test_directed.rs
rename to crates/entry/tests/test_directed.BAK
index 7797984..9f18d45 100644
--- a/crates/graphmap/tests/test_directed.rs
+++ b/crates/entry/tests/test_directed.BAK
@@ -5,7 +5,7 @@
 };
 #[cfg(feature = "convert")]
 use petgraph_graph::{Graph, NodeIndex};
-use petgraph_graphmap::{DiGraphMap, GraphMap};
+use petgraph_graphmap::{DiGraphMap, EntryStorage};
 
 #[test]
 fn self_loop() {
@@ -97,11 +97,10 @@
     assert_eq!(graphmap.edges(1).collect::<Vec<_>>(), vec![(1, 2, &-2)]);
     assert_eq!(graphmap.edges(2).collect::<Vec<_>>(), vec![(2, 0, &-3)]);
 
-    assert_eq!(graphmap.all_edges().collect::<Vec<_>>(), vec![
-        (0, 1, &-1),
-        (1, 2, &-2),
-        (2, 0, &-3),
-    ]);
+    assert_eq!(
+        graphmap.all_edges().collect::<Vec<_>>(),
+        vec![(0, 1, &-1), (1, 2, &-2), (2, 0, &-3),]
+    );
 }
 
 #[test]
@@ -121,12 +120,15 @@
     assert_eq!(graph.node_count(), 4);
     assert_eq!(graph.edge_count(), 3);
 
-    assert_eq!(graph.node_references().collect::<Vec<_>>(), vec![
-        (NodeIndex::new(0), &0),
-        (NodeIndex::new(1), &1),
-        (NodeIndex::new(2), &2),
-        (NodeIndex::new(3), &3),
-    ]);
+    assert_eq!(
+        graph.node_references().collect::<Vec<_>>(),
+        vec![
+            (NodeIndex::new(0), &0),
+            (NodeIndex::new(1), &1),
+            (NodeIndex::new(2), &2),
+            (NodeIndex::new(3), &3),
+        ]
+    );
 
     assert_eq!(
         graph
diff --git a/crates/graphmap/tests/test_proptest.rs b/crates/entry/tests/test_proptest.BAK
similarity index 85%
rename from crates/graphmap/tests/test_proptest.rs
rename to crates/entry/tests/test_proptest.BAK
index 57872d7..ffd0de2 100644
--- a/crates/graphmap/tests/test_proptest.rs
+++ b/crates/entry/tests/test_proptest.BAK
@@ -7,10 +7,10 @@
     edge::{Directed, EdgeType, Undirected},
     visit::IntoNodeIdentifiers,
 };
-use petgraph_graphmap::{GraphMap, NodeTrait};
+use petgraph_graphmap::{EntryStorage, NodeTrait};
 use proptest::prelude::*;
 
-fn assert_graphmap_consistent<N, E, Ty>(g: &GraphMap<N, E, Ty>)
+fn assert_graphmap_consistent<N, E, Ty>(g: &EntryStorage<N, E, Ty>)
 where
     Ty: EdgeType,
     N: NodeTrait + fmt::Debug,
@@ -36,7 +36,7 @@
     }
 }
 
-fn remove_node<Ty>(graph: &mut GraphMap<i8, (), Ty>, node: i8)
+fn remove_node<Ty>(graph: &mut EntryStorage<i8, (), Ty>, node: i8)
 where
     Ty: EdgeType,
 {
@@ -49,7 +49,7 @@
     assert!(!graph.contains_node(node));
 }
 
-fn remove_edge<Ty>(graph: &mut GraphMap<i8, (), Ty>, a: i8, b: i8)
+fn remove_edge<Ty>(graph: &mut EntryStorage<i8, (), Ty>, a: i8, b: i8)
 where
     Ty: EdgeType,
 {
@@ -64,7 +64,7 @@
     assert!(!graph.neighbors(a).any(|x| x == b));
 }
 
-fn add_remove_edge<Ty>(graph: &mut GraphMap<i8, (), Ty>, a: i8, b: i8)
+fn add_remove_edge<Ty>(graph: &mut EntryStorage<i8, (), Ty>, a: i8, b: i8)
 where
     Ty: EdgeType,
 {
@@ -90,7 +90,7 @@
     }
 }
 
-fn find_free_edge<Ty>(graph: &GraphMap<i8, (), Ty>) -> (i8, i8)
+fn find_free_edge<Ty>(graph: &EntryStorage<i8, (), Ty>) -> (i8, i8)
 where
     Ty: EdgeType,
 {
@@ -107,11 +107,11 @@
     panic!("no free edge found");
 }
 
-fn graph_and_node<Ty>() -> impl Strategy<Value = (GraphMap<i8, (), Ty>, i8)>
+fn graph_and_node<Ty>() -> impl Strategy<Value = (EntryStorage<i8, (), Ty>, i8)>
 where
     Ty: EdgeType + Clone + 'static,
 {
-    any::<GraphMap<i8, (), Ty>>()
+    any::<EntryStorage<i8, (), Ty>>()
         .prop_filter("graph must have nodes", |graph| graph.node_count() > 0)
         .prop_flat_map(|graph| {
             let nodes = graph.node_count();
@@ -125,11 +125,11 @@
         })
 }
 
-fn graph_and_edge<Ty>() -> impl Strategy<Value = (GraphMap<i8, (), Ty>, (i8, i8))>
+fn graph_and_edge<Ty>() -> impl Strategy<Value = (EntryStorage<i8, (), Ty>, (i8, i8))>
 where
     Ty: EdgeType + Clone + 'static,
 {
-    any::<GraphMap<i8, (), Ty>>()
+    any::<EntryStorage<i8, (), Ty>>()
         .prop_filter("graph must have edges", |graph| graph.edge_count() > 0)
         .prop_flat_map(|graph| {
             let edges = graph.edge_count();
@@ -143,11 +143,11 @@
         })
 }
 
-fn at_least_one_free_edge<Ty>() -> impl Strategy<Value = GraphMap<i8, (), Ty>>
+fn at_least_one_free_edge<Ty>() -> impl Strategy<Value = EntryStorage<i8, (), Ty>>
 where
     Ty: EdgeType + 'static,
 {
-    any::<GraphMap<i8, (), Ty>>()
+    any::<EntryStorage<i8, (), Ty>>()
         .prop_filter("graph must have at least one node", |graph| {
             graph.node_count() > 0
         })
diff --git a/crates/graphmap/tests/test_serde.rs b/crates/entry/tests/test_serde.BAK
similarity index 81%
rename from crates/graphmap/tests/test_serde.rs
rename to crates/entry/tests/test_serde.BAK
index 0486560..4f16001 100644
--- a/crates/graphmap/tests/test_serde.rs
+++ b/crates/entry/tests/test_serde.BAK
@@ -5,15 +5,15 @@
 use insta::assert_json_snapshot;
 use petgraph_core::edge::{Directed, EdgeType};
 use petgraph_graph::Graph;
-use petgraph_graphmap::{GraphMap, NodeTrait};
+use petgraph_graphmap::{EntryStorage, NodeTrait};
 #[cfg(feature = "proptest")]
 use proptest::prelude::*;
 use serde::{Deserialize, Serialize};
 
-fn example() -> GraphMap<i32, u32, Directed> {
+fn example() -> EntryStorage<i32, u32, Directed> {
     let [a, b, c, d, e, f] = [0, 1, 2, 3, 4, 5];
 
-    let mut graph = GraphMap::from_edges([
+    let mut graph = EntryStorage::from_edges([
         (a, b, 7),
         (c, a, 9),
         (a, d, 14),
@@ -31,7 +31,7 @@
     graph
 }
 
-fn assert_graph_eq<N, E, Ty>(graph1: &GraphMap<N, E, Ty>, graph2: &GraphMap<N, E, Ty>)
+fn assert_graph_eq<N, E, Ty>(graph1: &EntryStorage<N, E, Ty>, graph2: &EntryStorage<N, E, Ty>)
 where
     N: NodeTrait + PartialEq + Debug,
     E: PartialEq + Debug,
@@ -60,7 +60,8 @@
 fn deserialize() {
     let graph = example();
     let serialized = serde_value::to_value(&graph).unwrap();
-    let deserialized: GraphMap<i32, u32, Directed> = GraphMap::deserialize(serialized).unwrap();
+    let deserialized: EntryStorage<i32, u32, Directed> =
+        EntryStorage::deserialize(serialized).unwrap();
 
     assert_graph_eq(&graph, &deserialized);
 }
@@ -71,14 +72,14 @@
     pub y: i32,
 }
 
-fn example_struct() -> GraphMap<Point, i32, Directed> {
+fn example_struct() -> EntryStorage<Point, i32, Directed> {
     let a = Point { x: 0, y: 0 };
     let b = Point { x: 1, y: 1 };
     let c = Point { x: 2, y: 2 };
 
     let d = Point { x: 2, y: 1 };
 
-    let mut graph = GraphMap::from_edges([
+    let mut graph = EntryStorage::from_edges([
         (a, b, 1), //
         (b, c, 2),
         (c, a, 3),
@@ -102,7 +103,8 @@
     let graph = example_struct();
 
     let serialized = serde_value::to_value(&graph).unwrap();
-    let deserialized: GraphMap<Point, i32, Directed> = GraphMap::deserialize(serialized).unwrap();
+    let deserialized: EntryStorage<Point, i32, Directed> =
+        EntryStorage::deserialize(serialized).unwrap();
 
     assert_graph_eq(&graph, &deserialized);
 }
diff --git a/crates/graphmap/tests/test_undirected.rs b/crates/entry/tests/test_undirected.BAK
similarity index 100%
rename from crates/graphmap/tests/test_undirected.rs
rename to crates/entry/tests/test_undirected.BAK
diff --git a/crates/graphmap/Cargo.toml b/crates/graphmap/Cargo.toml
deleted file mode 100644
index 3e271bc..0000000
--- a/crates/graphmap/Cargo.toml
+++ /dev/null
@@ -1,28 +0,0 @@
-[package]
-name = "petgraph-graphmap"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-[dependencies]
-fixedbitset.workspace = true
-funty.workspace = true
-indexmap.workspace = true
-petgraph-core = { workspace = true, features = ["alloc", "indexmap"] }
-petgraph-graph = { workspace = true, optional = true }
-serde = { workspace = true, optional = true, default-features = false, features = ["alloc"] }
-petgraph-proptest = { workspace = true, default-features = false, optional = true }
-proptest = { workspace = true, default-features = false, optional = true }
-fxhash = "0.2.1"
-
-[dev-dependencies]
-serde = { version = "1.0.130", features = ["derive"] }
-serde-value = "0.7.0"
-insta = { version = "1.29.0", features = ['serde', 'json'] }
-
-[features]
-convert = ["petgraph-graph"]
-default = ["std"]
-serde = ["dep:serde", "convert", "petgraph-graph?/serde"]
-std = ["petgraph-core/std", "indexmap/std", "serde?/std"]
-proptest = ["dep:proptest", "dep:petgraph-proptest"]
diff --git a/crates/graphmap/src/lib.rs b/crates/graphmap/src/lib.rs
deleted file mode 100644
index e6f2526..0000000
--- a/crates/graphmap/src/lib.rs
+++ /dev/null
@@ -1,1241 +0,0 @@
-#![cfg_attr(not(feature = "std"), no_std)]
-//! `GraphMap<N, E, Ty>` is a graph datastructure where node values are mapping
-//! keys.
-
-#[cfg(feature = "proptest")]
-mod proptest;
-
-extern crate alloc;
-extern crate core;
-
-use alloc::vec::Vec;
-use core::{
-    cmp::Ordering,
-    fmt, hash,
-    hash::Hash,
-    iter,
-    marker::PhantomData,
-    mem,
-    ops::{Deref, Index, IndexMut},
-    slice,
-};
-
-use fxhash::FxBuildHasher;
-use indexmap::map::{Iter as IndexMapIter, IterMut as IndexMapIterMut, Keys};
-use petgraph_core::{
-    deprecated::{
-        edge::{Directed, EdgeType, Undirected},
-        index::IndexType,
-        visit, IntoWeightedEdge,
-    },
-    edge::Direction,
-    iterator_wrap,
-};
-
-type IndexMap<K, V> = indexmap::IndexMap<K, V, FxBuildHasher>;
-type IndexSet<K> = indexmap::IndexSet<K, FxBuildHasher>;
-
-#[cfg(feature = "convert")]
-use petgraph_graph::{node_index, Graph};
-
-/// A `GraphMap` with undirected edges.
-///
-/// For example, an edge between *1* and *2* is equivalent to an edge between
-/// *2* and *1*.
-pub type UnGraphMap<N, E> = GraphMap<N, E, Undirected>;
-
-/// A `GraphMap` with directed edges.
-///
-/// For example, an edge from *1* to *2* is distinct from an edge from *2* to
-/// *1*.
-pub type DiGraphMap<N, E> = GraphMap<N, E, Directed>;
-
-/// `GraphMap<N, E, Ty>` is a graph datastructure using an associative array
-/// of its node weights `N`.
-///
-/// It uses an combined adjacency list and sparse adjacency matrix
-/// representation, using **O(|V| + |E|)** space, and allows testing for edge
-/// existence in constant time.
-///
-/// `GraphMap` is parameterized over:
-///
-/// - Associated data `N` for nodes and `E` for edges, called *weights*.
-/// - The node weight `N` must implement `Copy` and will be used as node
-/// identifier, duplicated into several places in the data structure.
-/// It must be suitable as a hash table key (implementing `Eq + Hash`).
-/// The node type must also implement `Ord` so that the implementation can
-/// order the pair (`a`, `b`) for an edge connecting any two nodes `a` and `b`.
-/// - `E` can be of arbitrary type.
-/// - Edge type `Ty` that determines whether the graph edges are directed or
-/// undirected.
-///
-/// You can use the type aliases `UnGraphMap` and `DiGraphMap` for convenience.
-///
-/// `GraphMap` does not allow parallel edges, but self loops are allowed.
-///
-/// Depends on crate feature `graphmap` (default).
-#[derive(Clone)]
-pub struct GraphMap<N, E, Ty> {
-    nodes: IndexMap<N, Vec<(N, Direction)>>,
-    edges: IndexMap<(N, N), E>,
-    ty: PhantomData<Ty>,
-}
-
-impl<N: fmt::Debug, E: fmt::Debug, Ty: EdgeType> fmt::Debug for GraphMap<N, E, Ty> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        fmt::Debug::fmt(&self.nodes, f)
-    }
-}
-
-/// A trait group for `GraphMap`'s node identifier.
-pub trait NodeTrait: Copy + Ord + Hash {}
-impl<N> NodeTrait for N where N: Copy + Ord + Hash {}
-
-#[cfg(feature = "serde")]
-impl<N, E, Ty> serde::Serialize for GraphMap<N, E, Ty>
-where
-    Ty: EdgeType,
-    N: NodeTrait + serde::Serialize,
-    E: serde::Serialize,
-    GraphMap<N, E, Ty>: Clone,
-{
-    /// Serializes the given `GraphMap` into the same format as the standard
-    /// `Graph`. Needs feature `serde-1`.
-    ///
-    /// Note: the graph has to be `Clone` for this to work.
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        let cloned_graph: GraphMap<N, E, Ty> = GraphMap::clone(self);
-        let equivalent_graph: Graph<N, E, Ty, u32> = cloned_graph.into_graph();
-        equivalent_graph.serialize(serializer)
-    }
-}
-
-#[cfg(feature = "serde")]
-impl<'de, N, E, Ty> serde::Deserialize<'de> for GraphMap<N, E, Ty>
-where
-    Ty: EdgeType,
-    N: NodeTrait + serde::Deserialize<'de>,
-    E: Clone + serde::Deserialize<'de>,
-{
-    /// Deserializes into a new `GraphMap` from the same format as the standard
-    /// `Graph`. Needs feature `serde-1`.
-    ///
-    /// **Warning**: When deseralizing a graph that was not originally a `GraphMap`,
-    /// the restrictions from [`from_graph`](#method.from_graph) apply.
-    ///
-    /// Note: The edge weights have to be `Clone` for this to work.
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let equivalent_graph: Graph<N, E, Ty, u32> = Graph::deserialize(deserializer)?;
-        Ok(GraphMap::from_graph(equivalent_graph))
-    }
-}
-
-impl<N, E, Ty> GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    /// Create a new `GraphMap`
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    /// Create a new `GraphMap` with estimated capacity.
-    pub fn with_capacity(nodes: usize, edges: usize) -> Self {
-        GraphMap {
-            nodes: IndexMap::with_capacity_and_hasher(nodes, FxBuildHasher::default()),
-            edges: IndexMap::with_capacity_and_hasher(edges, FxBuildHasher::default()),
-            ty: PhantomData,
-        }
-    }
-
-    /// Return the current node and edge capacity of the graph.
-    pub fn capacity(&self) -> (usize, usize) {
-        (self.nodes.capacity(), self.edges.capacity())
-    }
-
-    /// Use their natural order to map the node pair (a, b) to a canonical edge id.
-    #[inline]
-    fn edge_key(a: N, b: N) -> (N, N) {
-        if Ty::is_directed() || a <= b {
-            (a, b)
-        } else {
-            (b, a)
-        }
-    }
-
-    /// Whether the graph has directed edges.
-    pub fn is_directed(&self) -> bool {
-        Ty::is_directed()
-    }
-
-    /// Create a new `GraphMap` from an iterable of edges.
-    ///
-    /// Node values are taken directly from the list.
-    /// Edge weights `E` may either be specified in the list,
-    /// or they are filled with default values.
-    ///
-    /// Nodes are inserted automatically to match the edges.
-    ///
-    /// ```
-    /// use petgraph_graphmap::UnGraphMap;
-    ///
-    /// // Create a new undirected GraphMap.
-    /// // Use a type hint to have `()` be the edge weight type.
-    /// let gr = UnGraphMap::<_, ()>::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
-    /// ```
-    pub fn from_edges<I>(iterable: I) -> Self
-    where
-        I: IntoIterator,
-        I::Item: IntoWeightedEdge<E, NodeId = N>,
-    {
-        Self::from_iter(iterable)
-    }
-
-    /// Return the number of nodes in the graph.
-    pub fn node_count(&self) -> usize {
-        self.nodes.len()
-    }
-
-    /// Return the number of edges in the graph.
-    pub fn edge_count(&self) -> usize {
-        self.edges.len()
-    }
-
-    /// Remove all nodes and edges
-    pub fn clear(&mut self) {
-        self.nodes.clear();
-        self.edges.clear();
-    }
-
-    /// Add node `n` to the graph.
-    pub fn add_node(&mut self, n: N) -> N {
-        self.nodes.entry(n).or_insert(Vec::new());
-        n
-    }
-
-    /// Return `true` if node `n` was removed.
-    ///
-    /// Computes in **O(V)** time, due to the removal of edges with other nodes.
-    pub fn remove_node(&mut self, n: N) -> bool {
-        let links = match self.nodes.swap_remove(&n) {
-            None => return false,
-            Some(sus) => sus,
-        };
-        for (succ, dir) in links {
-            let edge = if dir == Direction::Outgoing {
-                Self::edge_key(n, succ)
-            } else {
-                Self::edge_key(succ, n)
-            };
-            // remove all successor links
-            self.remove_single_edge(&succ, &n, dir.reverse());
-            // Remove all edge values
-            self.edges.swap_remove(&edge);
-        }
-        true
-    }
-
-    /// Return `true` if the node is contained in the graph.
-    pub fn contains_node(&self, n: N) -> bool {
-        self.nodes.contains_key(&n)
-    }
-
-    /// Add an edge connecting `a` and `b` to the graph, with associated
-    /// data `weight`. For a directed graph, the edge is directed from `a`
-    /// to `b`.
-    ///
-    /// Inserts nodes `a` and/or `b` if they aren't already part of the graph.
-    ///
-    /// Return `None` if the edge did not previously exist, otherwise,
-    /// the associated data is updated and the old value is returned
-    /// as `Some(old_weight)`.
-    ///
-    /// ```
-    /// // Create a GraphMap with directed edges, and add one edge to it
-    /// use petgraph_graphmap::DiGraphMap;
-    ///
-    /// let mut g = DiGraphMap::new();
-    /// g.add_edge("x", "y", -1);
-    /// assert_eq!(g.node_count(), 2);
-    /// assert_eq!(g.edge_count(), 1);
-    /// assert!(g.contains_edge("x", "y"));
-    /// assert!(!g.contains_edge("y", "x"));
-    /// ```
-    pub fn add_edge(&mut self, a: N, b: N, weight: E) -> Option<E> {
-        if let old @ Some(_) = self.edges.insert(Self::edge_key(a, b), weight) {
-            old
-        } else {
-            // insert in the adjacency list if it's a new edge
-            self.nodes
-                .entry(a)
-                .or_insert_with(|| Vec::with_capacity(1))
-                .push((b, Direction::Outgoing));
-            if a != b {
-                // self loops don't have the Incoming entry
-                self.nodes
-                    .entry(b)
-                    .or_insert_with(|| Vec::with_capacity(1))
-                    .push((a, Direction::Incoming));
-            }
-            None
-        }
-    }
-
-    /// Remove edge relation from a to b
-    ///
-    /// Return `true` if it did exist.
-    fn remove_single_edge(&mut self, a: &N, b: &N, dir: Direction) -> bool {
-        match self.nodes.get_mut(a) {
-            None => false,
-            Some(sus) => {
-                if Ty::is_directed() {
-                    match sus.iter().position(|elt| elt == &(*b, dir)) {
-                        Some(index) => {
-                            sus.swap_remove(index);
-                            true
-                        }
-                        None => false,
-                    }
-                } else {
-                    match sus.iter().position(|elt| &elt.0 == b) {
-                        Some(index) => {
-                            sus.swap_remove(index);
-                            true
-                        }
-                        None => false,
-                    }
-                }
-            }
-        }
-    }
-
-    /// Remove edge from `a` to `b` from the graph and return the edge weight.
-    ///
-    /// Return `None` if the edge didn't exist.
-    ///
-    /// ```
-    /// // Create a GraphMap with undirected edges, and add and remove an edge.
-    /// use petgraph_graphmap::UnGraphMap;
-    ///
-    /// let mut g = UnGraphMap::new();
-    /// g.add_edge("x", "y", -1);
-    ///
-    /// let edge_data = g.remove_edge("y", "x");
-    /// assert_eq!(edge_data, Some(-1));
-    /// assert_eq!(g.edge_count(), 0);
-    /// ```
-    pub fn remove_edge(&mut self, a: N, b: N) -> Option<E> {
-        let exist1 = self.remove_single_edge(&a, &b, Direction::Outgoing);
-        let exist2 = if a != b {
-            self.remove_single_edge(&b, &a, Direction::Incoming)
-        } else {
-            exist1
-        };
-        let weight = self.edges.remove(&Self::edge_key(a, b));
-        debug_assert!(exist1 == exist2 && exist1 == weight.is_some());
-        weight
-    }
-
-    /// Return `true` if the edge connecting `a` with `b` is contained in the graph.
-    pub fn contains_edge(&self, a: N, b: N) -> bool {
-        self.edges.contains_key(&Self::edge_key(a, b))
-    }
-
-    /// Return an iterator over the nodes of the graph.
-    ///
-    /// Iterator element type is `N`.
-    pub fn nodes(&self) -> Nodes<N> {
-        Nodes {
-            iter: self.nodes.keys().cloned(),
-        }
-    }
-
-    /// Return an iterator of all nodes with an edge starting from `a`.
-    ///
-    /// - `Directed`: Outgoing edges from `a`.
-    /// - `Undirected`: All edges from or to `a`.
-    ///
-    /// Produces an empty iterator if the node doesn't exist.<br>
-    /// Iterator element type is `N`.
-    pub fn neighbors(&self, a: N) -> Neighbors<N, Ty> {
-        Neighbors {
-            iter: match self.nodes.get(&a) {
-                Some(neigh) => neigh.iter(),
-                None => [].iter(),
-            },
-            ty: self.ty,
-        }
-    }
-
-    /// Return an iterator of all neighbors that have an edge between them and
-    /// `a`, in the specified direction.
-    /// If the graph's edges are undirected, this is equivalent to *.neighbors(a)*.
-    ///
-    /// - `Directed`, `Outgoing`: All edges from `a`.
-    /// - `Directed`, `Incoming`: All edges to `a`.
-    /// - `Undirected`: All edges from or to `a`.
-    ///
-    /// Produces an empty iterator if the node doesn't exist.<br>
-    /// Iterator element type is `N`.
-    pub fn neighbors_directed(&self, a: N, dir: Direction) -> NeighborsDirected<N, Ty> {
-        NeighborsDirected {
-            iter: match self.nodes.get(&a) {
-                Some(neigh) => neigh.iter(),
-                None => [].iter(),
-            },
-            start_node: a,
-            dir,
-            ty: self.ty,
-        }
-    }
-
-    /// Return an iterator of target nodes with an edge starting from `a`,
-    /// paired with their respective edge weights.
-    ///
-    /// - `Directed`: Outgoing edges from `a`.
-    /// - `Undirected`: All edges from or to `a`.
-    ///
-    /// Produces an empty iterator if the node doesn't exist.<br>
-    /// Iterator element type is `(N, N, &E)`.
-    pub fn edges(&self, a: N) -> Edges<N, E, Ty> {
-        Edges {
-            from: a,
-            iter: self.neighbors(a),
-            edges: &self.edges,
-        }
-    }
-
-    /// Return an iterator of target nodes with an edge starting from `a`,
-    /// paired with their respective edge weights.
-    ///
-    /// - `Directed`, `Outgoing`: All edges from `a`.
-    /// - `Directed`, `Incoming`: All edges to `a`.
-    /// - `Undirected`, `Outgoing`: All edges connected to `a`, with `a` being the source of each
-    ///   edge.
-    /// - `Undirected`, `Incoming`: All edges connected to `a`, with `a` being the target of each
-    ///   edge.
-    ///
-    /// Produces an empty iterator if the node doesn't exist.<br>
-    /// Iterator element type is `(N, N, &E)`.
-    pub fn edges_directed(&self, a: N, dir: Direction) -> EdgesDirected<N, E, Ty> {
-        EdgesDirected {
-            from: a,
-            iter: self.neighbors_directed(a, dir),
-            dir,
-            edges: &self.edges,
-        }
-    }
-
-    /// Return a reference to the edge weight connecting `a` with `b`, or
-    /// `None` if the edge does not exist in the graph.
-    pub fn edge_weight(&self, a: N, b: N) -> Option<&E> {
-        self.edges.get(&Self::edge_key(a, b))
-    }
-
-    /// Return a mutable reference to the edge weight connecting `a` with `b`, or
-    /// `None` if the edge does not exist in the graph.
-    pub fn edge_weight_mut(&mut self, a: N, b: N) -> Option<&mut E> {
-        self.edges.get_mut(&Self::edge_key(a, b))
-    }
-
-    /// Return an iterator over all edges of the graph with their weight in arbitrary order.
-    ///
-    /// Iterator element type is `(N, N, &E)`
-    pub fn all_edges(&self) -> AllEdges<N, E, Ty> {
-        AllEdges {
-            inner: self.edges.iter(),
-            ty: self.ty,
-        }
-    }
-
-    /// Return an iterator over all edges of the graph in arbitrary order, with a mutable reference
-    /// to their weight.
-    ///
-    /// Iterator element type is `(N, N, &mut E)`
-    pub fn all_edges_mut(&mut self) -> AllEdgesMut<N, E, Ty> {
-        AllEdgesMut {
-            inner: self.edges.iter_mut(),
-            ty: self.ty,
-        }
-    }
-
-    /// Return a `Graph` that corresponds to this `GraphMap`.
-    ///
-    /// 1. Note that node and edge indices in the `Graph` have nothing in common with the
-    ///    `GraphMap`s node weights `N`. The node weights `N` are used as node weights in the
-    ///    resulting `Graph`, too.
-    /// 2. Note that the index type is user-chosen.
-    ///
-    /// Computes in **O(|V| + |E|)** time (average).
-    ///
-    /// **Panics** if the number of nodes or edges does not fit with
-    /// the resulting graph's index type.
-    #[cfg(feature = "convert")]
-    pub fn into_graph<Ix>(self) -> Graph<N, E, Ty, Ix>
-    where
-        Ix: IndexType,
-    {
-        // assuming two successive iterations of the same hashmap produce the same order
-        let mut gr = Graph::with_capacity(self.node_count(), self.edge_count());
-        for (&node, _) in &self.nodes {
-            gr.add_node(node);
-        }
-        for ((a, b), edge_weight) in self.edges {
-            let (ai, ..) = self.nodes.get_full(&a).unwrap();
-            let (bi, ..) = self.nodes.get_full(&b).unwrap();
-            gr.add_edge(node_index(ai), node_index(bi), edge_weight);
-        }
-        gr
-    }
-
-    /// Creates a `GraphMap` that corresponds to the given `Graph`.
-    ///
-    /// **Warning**: Nodes with the same weight are merged and only the last parallel edge
-    /// is kept. Node and edge indices of the `Graph` are lost. Only use this function
-    /// if the node weights are distinct and there are no parallel edges.
-    ///
-    /// Computes in **O(|V| + |E|)** time (average).
-    #[cfg(feature = "convert")]
-    pub fn from_graph<Ix>(graph: Graph<N, E, Ty, Ix>) -> Self
-    where
-        Ix: IndexType,
-        E: Clone,
-    {
-        let mut new_graph: GraphMap<N, E, Ty> =
-            GraphMap::with_capacity(graph.node_count(), graph.edge_count());
-
-        for node in graph.raw_nodes() {
-            new_graph.add_node(node.weight);
-        }
-
-        for edge in graph.edge_indices() {
-            let (a, b) = graph.edge_endpoints(edge).unwrap();
-            new_graph.add_edge(
-                *graph.node_weight(a).unwrap(),
-                *graph.node_weight(b).unwrap(),
-                graph.edge_weight(edge).unwrap().clone(),
-            );
-        }
-
-        new_graph
-    }
-}
-
-/// Create a new `GraphMap` from an iterable of edges.
-impl<N, E, Ty, Item> FromIterator<Item> for GraphMap<N, E, Ty>
-where
-    Item: IntoWeightedEdge<E, NodeId = N>,
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn from_iter<I>(iterable: I) -> Self
-    where
-        I: IntoIterator<Item = Item>,
-    {
-        let iter = iterable.into_iter();
-        let (low, _) = iter.size_hint();
-        let mut g = Self::with_capacity(0, low);
-        g.extend(iter);
-        g
-    }
-}
-
-/// Extend the graph from an iterable of edges.
-///
-/// Nodes are inserted automatically to match the edges.
-impl<N, E, Ty, Item> Extend<Item> for GraphMap<N, E, Ty>
-where
-    Item: IntoWeightedEdge<E, NodeId = N>,
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn extend<I>(&mut self, iterable: I)
-    where
-        I: IntoIterator<Item = Item>,
-    {
-        let iter = iterable.into_iter();
-        let (low, _) = iter.size_hint();
-        self.edges.reserve(low);
-
-        for elt in iter {
-            let (source, target, weight) = elt.into_weighted_edge();
-            self.add_edge(source, target, weight);
-        }
-    }
-}
-
-iterator_wrap! {
-    impl (Iterator DoubleEndedIterator ExactSizeIterator) for
-    #[derive(Debug, Clone)]
-    struct Nodes <'a, N> where { N: 'a + NodeTrait }
-    item: N,
-    iter: iter::Cloned<Keys<'a, N, Vec<(N, Direction)>>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct Neighbors<'a, N, Ty = Undirected>
-where
-    N: 'a,
-    Ty: EdgeType,
-{
-    iter: slice::Iter<'a, (N, Direction)>,
-    ty: PhantomData<Ty>,
-}
-
-impl<'a, N, Ty> Iterator for Neighbors<'a, N, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type Item = N;
-
-    fn next(&mut self) -> Option<N> {
-        if Ty::is_directed() {
-            (&mut self.iter)
-                .filter_map(|&(n, dir)| {
-                    if dir == Direction::Outgoing {
-                        Some(n)
-                    } else {
-                        None
-                    }
-                })
-                .next()
-        } else {
-            self.iter.next().map(|&(n, _)| n)
-        }
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        let (lower, upper) = self.iter.size_hint();
-        if Ty::is_directed() {
-            (0, upper)
-        } else {
-            (lower, upper)
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct NeighborsDirected<'a, N, Ty>
-where
-    N: 'a,
-    Ty: EdgeType,
-{
-    iter: slice::Iter<'a, (N, Direction)>,
-    start_node: N,
-    dir: Direction,
-    ty: PhantomData<Ty>,
-}
-
-impl<'a, N, Ty> Iterator for NeighborsDirected<'a, N, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type Item = N;
-
-    fn next(&mut self) -> Option<N> {
-        if Ty::is_directed() {
-            let self_dir = self.dir;
-            let start_node = self.start_node;
-            (&mut self.iter)
-                .filter_map(move |&(n, dir)| {
-                    if dir == self_dir || n == start_node {
-                        Some(n)
-                    } else {
-                        None
-                    }
-                })
-                .next()
-        } else {
-            self.iter.next().map(|&(n, _)| n)
-        }
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        let (lower, upper) = self.iter.size_hint();
-        if Ty::is_directed() {
-            (0, upper)
-        } else {
-            (lower, upper)
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct Edges<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-    Ty: EdgeType,
-{
-    from: N,
-    edges: &'a IndexMap<(N, N), E>,
-    iter: Neighbors<'a, N, Ty>,
-}
-
-impl<'a, N, E, Ty> Iterator for Edges<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = (N, N, &'a E);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|b| {
-            let a = self.from;
-            match self.edges.get(&GraphMap::<N, E, Ty>::edge_key(a, b)) {
-                None => unreachable!(),
-                Some(edge) => (a, b, edge),
-            }
-        })
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.iter.size_hint()
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct EdgesDirected<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-    Ty: EdgeType,
-{
-    from: N,
-    dir: Direction,
-    edges: &'a IndexMap<(N, N), E>,
-    iter: NeighborsDirected<'a, N, Ty>,
-}
-
-impl<'a, N, E, Ty> Iterator for EdgesDirected<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = (N, N, &'a E);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|mut b| {
-            let mut a = self.from;
-            if self.dir == Direction::Incoming {
-                mem::swap(&mut a, &mut b);
-            }
-            match self.edges.get(&GraphMap::<N, E, Ty>::edge_key(a, b)) {
-                None => unreachable!(),
-                Some(edge) => (a, b, edge),
-            }
-        })
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.iter.size_hint()
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct AllEdges<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-{
-    inner: IndexMapIter<'a, (N, N), E>,
-    ty: PhantomData<Ty>,
-}
-
-impl<'a, N, E, Ty> Iterator for AllEdges<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = (N, N, &'a E);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.inner.next().map(|(&(a, b), v)| (a, b, v))
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.inner.size_hint()
-    }
-
-    fn count(self) -> usize {
-        self.inner.count()
-    }
-
-    fn nth(&mut self, n: usize) -> Option<Self::Item> {
-        self.inner
-            .nth(n)
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-
-    fn last(self) -> Option<Self::Item> {
-        self.inner
-            .last()
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-}
-
-impl<'a, N, E, Ty> DoubleEndedIterator for AllEdges<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    fn next_back(&mut self) -> Option<Self::Item> {
-        self.inner
-            .next_back()
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-}
-
-pub struct AllEdgesMut<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-{
-    inner: IndexMapIterMut<'a, (N, N), E>, /* TODO: change to something that implements Debug +
-                                            * Clone? */
-    ty: PhantomData<Ty>,
-}
-
-impl<'a, N, E, Ty> Iterator for AllEdgesMut<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = (N, N, &'a mut E);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.inner
-            .next()
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.inner.size_hint()
-    }
-
-    fn count(self) -> usize {
-        self.inner.count()
-    }
-
-    fn nth(&mut self, n: usize) -> Option<Self::Item> {
-        self.inner
-            .nth(n)
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-
-    fn last(self) -> Option<Self::Item> {
-        self.inner
-            .last()
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-}
-
-impl<'a, N, E, Ty> DoubleEndedIterator for AllEdgesMut<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    fn next_back(&mut self) -> Option<Self::Item> {
-        self.inner
-            .next_back()
-            .map(|(&(n1, n2), weight)| (n1, n2, weight))
-    }
-}
-
-/// Index `GraphMap` by node pairs to access edge weights.
-impl<N, E, Ty> Index<(N, N)> for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type Output = E;
-
-    fn index(&self, index: (N, N)) -> &E {
-        let index = Self::edge_key(index.0, index.1);
-        self.edge_weight(index.0, index.1)
-            .expect("GraphMap::index: no such edge")
-    }
-}
-
-/// Index `GraphMap` by node pairs to access edge weights.
-impl<N, E, Ty> IndexMut<(N, N)> for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn index_mut(&mut self, index: (N, N)) -> &mut E {
-        let index = Self::edge_key(index.0, index.1);
-        self.edge_weight_mut(index.0, index.1)
-            .expect("GraphMap::index: no such edge")
-    }
-}
-
-/// Create a new empty `GraphMap`.
-impl<N, E, Ty> Default for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn default() -> Self {
-        GraphMap::with_capacity(0, 0)
-    }
-}
-
-/// A reference that is hashed and compared by its pointer value.
-///
-/// `Ptr` is used for certain configurations of `GraphMap`,
-/// in particular in the combination where the node type for
-/// `GraphMap` is something of type for example `Ptr(&Cell<T>)`,
-/// with the `Cell<T>` being `TypedArena` allocated.
-pub struct Ptr<'b, T: 'b>(pub &'b T);
-
-impl<'b, T> Copy for Ptr<'b, T> {}
-impl<'b, T> Clone for Ptr<'b, T> {
-    fn clone(&self) -> Self {
-        *self
-    }
-}
-
-fn ptr_eq<T>(a: *const T, b: *const T) -> bool {
-    a == b
-}
-
-impl<'b, T> PartialEq for Ptr<'b, T> {
-    /// Ptr compares by pointer equality, i.e if they point to the same value
-    fn eq(&self, other: &Ptr<'b, T>) -> bool {
-        ptr_eq(self.0, other.0)
-    }
-}
-
-impl<'b, T> PartialOrd for Ptr<'b, T> {
-    fn partial_cmp(&self, other: &Ptr<'b, T>) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl<'b, T> Ord for Ptr<'b, T> {
-    /// Ptr is ordered by pointer value, i.e. an arbitrary but stable and total order.
-    fn cmp(&self, other: &Ptr<'b, T>) -> Ordering {
-        let a: *const T = self.0;
-        let b: *const T = other.0;
-        a.cmp(&b)
-    }
-}
-
-impl<'b, T> Deref for Ptr<'b, T> {
-    type Target = T;
-
-    fn deref(&self) -> &T {
-        self.0
-    }
-}
-
-impl<'b, T> Eq for Ptr<'b, T> {}
-
-impl<'b, T> Hash for Ptr<'b, T> {
-    fn hash<H: hash::Hasher>(&self, st: &mut H) {
-        let ptr = (self.0) as *const T;
-        ptr.hash(st)
-    }
-}
-
-impl<'b, T: fmt::Debug> fmt::Debug for Ptr<'b, T> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct NodeIdentifiers<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-{
-    iter: IndexMapIter<'a, N, Vec<(N, Direction)>>,
-    ty: PhantomData<Ty>,
-    edge_ty: PhantomData<E>,
-}
-
-impl<'a, N, E, Ty> Iterator for NodeIdentifiers<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = N;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|(&n, _)| n)
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.iter.size_hint()
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct NodeReferences<'a, N, E: 'a, Ty>
-where
-    N: 'a + NodeTrait,
-{
-    iter: IndexMapIter<'a, N, Vec<(N, Direction)>>,
-    ty: PhantomData<Ty>,
-    edge_ty: PhantomData<E>,
-}
-
-impl<'a, N, E, Ty> Iterator for NodeReferences<'a, N, E, Ty>
-where
-    N: 'a + NodeTrait,
-    E: 'a,
-    Ty: EdgeType,
-{
-    type Item = (N, &'a N);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next().map(|(n, _)| (*n, n))
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        self.iter.size_hint()
-    }
-}
-
-impl<N, E, Ty> visit::GraphBase for GraphMap<N, E, Ty>
-where
-    N: Copy + PartialEq,
-{
-    type EdgeId = (N, N);
-    type NodeId = N;
-}
-
-impl<N, E, Ty> visit::Data for GraphMap<N, E, Ty>
-where
-    N: Copy + PartialEq,
-    Ty: EdgeType,
-{
-    type EdgeWeight = E;
-    type NodeWeight = N;
-}
-
-impl<N, E, Ty> visit::Visitable for GraphMap<N, E, Ty>
-where
-    N: Copy + Ord + Hash,
-    Ty: EdgeType,
-{
-    type Map = IndexSet<N>;
-
-    fn visit_map(&self) -> IndexSet<N> {
-        IndexSet::with_capacity_and_hasher(self.node_count(), FxBuildHasher::default())
-    }
-
-    fn reset_map(&self, map: &mut Self::Map) {
-        map.clear();
-    }
-}
-
-impl<N, E, Ty> visit::GraphProp for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type EdgeType = Ty;
-}
-
-impl<'a, N, E, Ty> visit::IntoNodeReferences for &'a GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type NodeRef = (N, &'a N);
-    type NodeReferences = NodeReferences<'a, N, E, Ty>;
-
-    fn node_references(self) -> Self::NodeReferences {
-        NodeReferences {
-            iter: self.nodes.iter(),
-            ty: self.ty,
-            edge_ty: PhantomData,
-        }
-    }
-}
-
-impl<'a, N, E: 'a, Ty> visit::IntoNodeIdentifiers for &'a GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type NodeIdentifiers = NodeIdentifiers<'a, N, E, Ty>;
-
-    fn node_identifiers(self) -> Self::NodeIdentifiers {
-        NodeIdentifiers {
-            iter: self.nodes.iter(),
-            ty: self.ty,
-            edge_ty: PhantomData,
-        }
-    }
-}
-
-impl<N, E, Ty> visit::NodeCount for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn node_count(&self) -> usize {
-        (*self).node_count()
-    }
-}
-
-impl<N, E, Ty> visit::NodeIndexable for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn node_bound(&self) -> usize {
-        self.node_count()
-    }
-
-    fn to_index(&self, ix: Self::NodeId) -> usize {
-        let (i, ..) = self.nodes.get_full(&ix).unwrap();
-        i
-    }
-
-    fn from_index(&self, ix: usize) -> Self::NodeId {
-        assert!(
-            ix < self.nodes.len(),
-            "The requested index {} is out-of-bounds.",
-            ix
-        );
-        let (&key, _) = self.nodes.get_index(ix).unwrap();
-        key
-    }
-}
-
-impl<N, E, Ty> visit::NodeCompactIndexable for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-}
-
-impl<'a, N: 'a, E, Ty> visit::IntoNeighbors for &'a GraphMap<N, E, Ty>
-where
-    N: Copy + Ord + Hash,
-    Ty: EdgeType,
-{
-    type Neighbors = Neighbors<'a, N, Ty>;
-
-    fn neighbors(self, n: Self::NodeId) -> Self::Neighbors {
-        self.neighbors(n)
-    }
-}
-
-impl<'a, N: 'a, E, Ty> visit::IntoNeighborsDirected for &'a GraphMap<N, E, Ty>
-where
-    N: Copy + Ord + Hash,
-    Ty: EdgeType,
-{
-    type NeighborsDirected = NeighborsDirected<'a, N, Ty>;
-
-    fn neighbors_directed(self, n: N, dir: Direction) -> Self::NeighborsDirected {
-        self.neighbors_directed(n, dir)
-    }
-}
-
-impl<N, E, Ty> visit::EdgeIndexable for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    fn edge_bound(&self) -> usize {
-        self.edge_count()
-    }
-
-    fn to_index(&self, ix: Self::EdgeId) -> usize {
-        let (i, ..) = self.edges.get_full(&ix).unwrap();
-        i
-    }
-
-    fn from_index(&self, ix: usize) -> Self::EdgeId {
-        assert!(
-            ix < self.edges.len(),
-            "The requested index {} is out-of-bounds.",
-            ix
-        );
-        let (&key, _) = self.edges.get_index(ix).unwrap();
-        key
-    }
-}
-
-impl<'a, N: 'a, E: 'a, Ty> visit::IntoEdges for &'a GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type Edges = Edges<'a, N, E, Ty>;
-
-    fn edges(self, a: Self::NodeId) -> Self::Edges {
-        self.edges(a)
-    }
-}
-
-impl<'a, N: 'a, E: 'a, Ty> visit::IntoEdgesDirected for &'a GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type EdgesDirected = EdgesDirected<'a, N, E, Ty>;
-
-    fn edges_directed(self, a: Self::NodeId, dir: Direction) -> Self::EdgesDirected {
-        self.edges_directed(a, dir)
-    }
-}
-
-impl<'a, N: 'a, E: 'a, Ty> visit::IntoEdgeReferences for &'a GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    type EdgeRef = (N, N, &'a E);
-    type EdgeReferences = AllEdges<'a, N, E, Ty>;
-
-    fn edge_references(self) -> Self::EdgeReferences {
-        self.all_edges()
-    }
-}
-
-impl<N, E, Ty> visit::EdgeCount for GraphMap<N, E, Ty>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    #[inline]
-    fn edge_count(&self) -> usize {
-        self.edge_count()
-    }
-}
-
-/// The `GraphMap` keeps an adjacency matrix internally.
-impl<N, E, Ty> visit::GetAdjacencyMatrix for GraphMap<N, E, Ty>
-where
-    N: Copy + Ord + Hash,
-    Ty: EdgeType,
-{
-    type AdjMatrix = ();
-
-    #[inline]
-    fn adjacency_matrix(&self) {}
-
-    #[inline]
-    fn is_adjacent(&self, _: &(), a: N, b: N) -> bool {
-        self.contains_edge(a, b)
-    }
-}
diff --git a/crates/graphmap/src/proptest.rs b/crates/graphmap/src/proptest.rs
deleted file mode 100644
index 12f3e9b..0000000
--- a/crates/graphmap/src/proptest.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use alloc::sync::Arc;
-use core::fmt::Debug;
-
-use petgraph_core::deprecated::edge::EdgeType;
-use petgraph_proptest::{default::graph_strategy_from_vtable, vtable::VTable};
-use proptest::{
-    arbitrary::Arbitrary, collection::SizeRange, prelude::BoxedStrategy, strategy::Strategy,
-};
-
-use crate::{GraphMap, NodeTrait};
-
-fn add_edge_no_return<N, E, Ty>(graph: &mut GraphMap<N, E, Ty>, a: N, b: N, weight: E)
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    graph.add_edge(a, b, weight);
-}
-
-// GraphMap does not implement `Create` or `Build`, so we need to use the vtable.
-fn create_vtable<N, E, Ty>() -> VTable<GraphMap<N, E, Ty>, N, N, E>
-where
-    N: NodeTrait,
-    Ty: EdgeType,
-{
-    VTable {
-        with_capacity: GraphMap::with_capacity,
-        add_node: GraphMap::add_node,
-        add_edge: add_edge_no_return::<N, E, Ty>,
-    }
-}
-
-impl<N, E, Ty> Arbitrary for GraphMap<N, E, Ty>
-where
-    N: NodeTrait + Arbitrary + Clone + Debug + 'static,
-    E: Arbitrary + Debug + 'static,
-    Ty: EdgeType + 'static,
-{
-    type Parameters = ();
-    // impl Strategy<Value = Self> is nightly, and therefore not usable here.
-    // TODO: revisit once impl_trait_in_assoc_type is stable. (https://github.com/rust-lang/rust/issues/63063)
-    type Strategy = BoxedStrategy<Self>;
-
-    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
-        // creating a graphmap with usize::MAX is a bit excessive, so we use u8::MAX instead.
-        graph_strategy_from_vtable(
-            create_vtable::<N, E, Ty>(),
-            true,
-            false,
-            0..=(u8::MAX as usize),
-            Some(Arc::new(|max| {
-                SizeRange::new(0..=usize::min(max.pow(2), u8::MAX as usize))
-            })),
-        )
-        .boxed()
-    }
-}
diff --git a/crates/numi/src/cast.rs b/crates/numi/src/cast.rs
index 66b562f..ad4c8d7 100644
--- a/crates/numi/src/cast.rs
+++ b/crates/numi/src/cast.rs
@@ -168,5 +168,5 @@
     assert_impl_all!(Saturating<u16>: CastTo<Wrapping<u32>>);
 
     #[test]
-    fn compile() {}
+    const fn compile() {}
 }
diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
index bd87a13..77a39ea 100644
--- a/crates/utils/src/lib.rs
+++ b/crates/utils/src/lib.rs
@@ -27,8 +27,8 @@
 
     (@collection: node $name:ident[$($id:ident : $attr:expr),* $(,)?]) => {
         #[allow(unreachable_pub)]
-        pub struct $name<T> {
-            $(pub $id: T,)*
+        pub struct $name {
+            $(pub $id: petgraph_core::node::NodeId,)*
         }
     };
 
@@ -45,8 +45,8 @@
         $name:ident[$($id:ident : $source:ident $(->)? $(--)? $target:ident : $attr:expr),* $(,)?]
     ) => {
         #[allow(unreachable_pub)]
-        pub struct $name<T> {
-            $(pub $id: T,)*
+        pub struct $name {
+            $(pub $id: petgraph_core::edge::EdgeId,)*
         }
     };
 
@@ -55,8 +55,8 @@
         $name:ident[$($id:ident : $source:ident $(->)? $(--)? $target:ident : @{$attr:expr}),* $(,)?]
     ) => {
         #[allow(unreachable_pub)]
-        pub struct $name<T> {
-            $(pub $id: T,)*
+        pub struct $name {
+            $(pub $id: petgraph_core::edge::EdgeId,)*
         }
     };
 
@@ -104,7 +104,7 @@
         };
     };
 
-    ($(#[$meta:meta])* $vis:vis factory($name:ident) => $graph:ty; [$($nodes:tt)*] as $nty:ty, [$($edges:tt)*] as $ety:ty) => {
+    ($(#[$meta:meta])* $vis:vis factory($name:ident) => $graph:ty; [$($nodes:tt)*], [$($edges:tt)*]) => {
         $(#[$meta])*
         $vis mod $name {
             use super::*;
@@ -114,7 +114,7 @@
 
             pub type Graph = $graph;
 
-            pub fn create() -> $crate::GraphCollection<$graph, NodeCollection<$nty>, EdgeCollection<$ety>> {
+            pub fn create() -> $crate::GraphCollection<$graph, NodeCollection, EdgeCollection> {
                 let mut graph = <$graph>::new();
 
                 $crate::graph!(@insert: node graph; nodes; NodeCollection[$($nodes)*]);
diff --git a/src/lib.rs b/src/lib.rs
index eae9b5f..3ff101d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -155,7 +155,7 @@
     pub use petgraph_csr::*;
 }
 
-#[cfg(feature = "graphmap")]
+#[cfg(feature = "entry")]
 pub mod graphmap {
     pub use petgraph_graphmap::*;
 }
diff --git a/src/prelude.rs b/src/prelude.rs
index 3d9ccbe..2d60704 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -20,6 +20,6 @@
 #[doc(no_inline)]
 pub use petgraph_graph::{DiGraph, EdgeIndex, Graph, NodeIndex, UnGraph};
 
-#[cfg(feature = "graphmap")]
+#[cfg(feature = "entry")]
 #[doc(no_inline)]
 pub use crate::graphmap::{DiGraphMap, GraphMap, UnGraphMap};