diff --git a/src/perf-tests/sets.ts b/src/perf-tests/sets.ts
index 3580392466e6df2b49fff85df07e42b08d85ff1b..f7f35b74cf6386e3954687b575795946d5b28939 100644
--- a/src/perf-tests/sets.ts
+++ b/src/perf-tests/sets.ts
@@ -1,5 +1,5 @@
 import * as B from 'benchmark'
-import IntPair from '../structure/collections/int-pair'
+import IntTuple from '../structure/collections/int-tuple'
 import OrdSet from '../structure/collections/ordered-set'
 import MSet from '../structure/collections/multi-set'
 
@@ -33,7 +33,7 @@ namespace Iteration {
 
     export function elementAt() {
         let s = 0;
-        for (let i = 0, _i = MSet.size(ms); i < _i; i++) s += IntPair.snd(MSet.get(ms, i));
+        for (let i = 0, _i = MSet.size(ms); i < _i; i++) s += IntTuple.snd(MSet.getAt(ms, i));
         return s;
     }
 
@@ -41,9 +41,9 @@ namespace Iteration {
         let s = 0;
         const keys = MSet.keys(ms);
         for (let i = 0, _i = OrdSet.size(keys); i < _i; i++) {
-            const set = MSet.getSetByKey(ms, OrdSet.get(keys, i));
+            const set = MSet.getByKey(ms, OrdSet.getAt(keys, i));
             for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                s += OrdSet.get(set, j);
+                s += OrdSet.getAt(set, j);
             }
         }
         return s;
@@ -51,10 +51,10 @@ namespace Iteration {
 
     export function manual1() {
         let s = 0;
-        for (let i = 0, _i = MSet.getSetCount(ms); i < _i; i++) {
-            const set = MSet.getSetByIndex(ms, i);
+        for (let i = 0, _i = MSet.keyCount(ms); i < _i; i++) {
+            const set = MSet.getByIndex(ms, i);
             for (let j = 0, _j = OrdSet.size(set); j < _j; j++) {
-                s += OrdSet.get(set, j);
+                s += OrdSet.getAt(set, j);
             }
         }
         return s;
@@ -75,4 +75,4 @@ suite
     .add('manual1', () => Iteration.manual1())
     .add('el at', () => Iteration.elementAt())
     .on('cycle', (e: any) => console.log(String(e.target)))
-    .run();
+    .run();
\ No newline at end of file
diff --git a/src/structure/collections/hash-functions.ts b/src/structure/collections/hash-functions.ts
index 542559ae50afd24fbf558d1c3118b34313855b3f..1394e40a453e42e9ca54f431169985a79f15a8db 100644
--- a/src/structure/collections/hash-functions.ts
+++ b/src/structure/collections/hash-functions.ts
@@ -43,4 +43,12 @@ export function hash4(i: number, j: number, k: number, l: number) {
     a = (a ^ 0xdeadbeef) + (a << 5);
     a = a ^ (a >> 11);
     return a;
+}
+
+/**
+ * A unique number for each pair of integers
+ * Biggest representable pair is (67108863, 67108863) (limit imposed by Number.MAX_SAFE_INTEGER)
+ */
+export function cantorPairing(a: number, b: number) {
+    return (a + b) * (a + b + 1) / 2 + b;
 }
\ No newline at end of file
diff --git a/src/structure/collections/int-pair.ts b/src/structure/collections/int-tuple.ts
similarity index 52%
rename from src/structure/collections/int-pair.ts
rename to src/structure/collections/int-tuple.ts
index 910e796fd6c147e18c6509ac96eb8ef6b6a61927..4b13b209636edc2ad318382357ccd7d3576e856b 100644
--- a/src/structure/collections/int-pair.ts
+++ b/src/structure/collections/int-tuple.ts
@@ -6,10 +6,14 @@
 
 import { hash2 } from './hash-functions'
 
-interface IntPair { fst: number, snd: number }
+/**
+ * Represents a pair of two integers as a double,
+ * Caution: === does not work, because of NaN, use IntTuple.areEqual for equality
+ */
+interface IntTuple extends Number { }
 
-namespace IntPair {
-    export interface Packed extends Number { }
+namespace IntTuple {
+    export interface Unpacked { fst: number, snd: number }
 
     const { _int32, _float64, _int32_1, _float64_1 } = (function() {
         const data = new ArrayBuffer(8);
@@ -22,72 +26,73 @@ namespace IntPair {
         };
     }());
 
-    export function is(x: any): x is IntPair {
+    export function is(x: any): x is Unpacked {
         return !!x && typeof x.fst === 'number' && typeof x.snd === 'number';
     }
 
     export function create(fst: number, snd: number) { return { fst, snd }; }
-    export function zero(): IntPair { return { fst: 0, snd: 0 }; }
+    export function zero(): Unpacked { return { fst: 0, snd: 0 }; }
 
-    export function pack1(fst: number, snd: number): number {
+    export function pack(fst: number, snd: number): IntTuple {
         _int32[0] = fst;
         _int32[1] = snd;
         return _float64[0];
     }
 
-    export function pack(p: IntPair): number {
-        _int32[0] = p.fst;
-        _int32[1] = p.snd;
+    export function pack1(t: Unpacked): IntTuple {
+        _int32[0] = t.fst;
+        _int32[1] = t.snd;
         return _float64[0];
     }
 
-    export function unpack(packed: Packed, target: IntPair): IntPair {
-        _float64[0] = packed as number;
+    export function unpack(t: IntTuple, target: Unpacked): Unpacked {
+        _float64[0] = t as number;
         target.fst = _int32[0];
         target.snd = _int32[1];
         return target;
     }
 
-    export function unpack1(packed: Packed): IntPair {
+    export function unpack1(packed: IntTuple): Unpacked {
         return unpack(packed, zero());
     }
 
-    export function fst(packed: Packed): number {
-        _float64[0] = packed as number;
+    export function fst(t: IntTuple): number {
+        _float64[0] = t as number;
         return _int32[0];
     }
 
-    export function snd(packed: Packed): number {
-        _float64[0] = packed as number;
+    export function snd(t: IntTuple): number {
+        _float64[0] = t as number;
         return _int32[1];
     }
 
-    export function areEqual(a: Packed, b: Packed) {
+    /** Normal equality does not work, because NaN === NaN ~> false */
+    export function areEqual(a: IntTuple, b: IntTuple) {
         _float64[0] = a as number;
         _float64_1[0] = b as number;
         return _int32[0] === _int32_1[0] && _int32[1] === _int32_1[1];
     }
 
-    export function compare(a: number, b: number) {
-        _float64[0] = a;
-        _float64_1[0] = b;
+    export function compare(a: IntTuple, b: IntTuple) {
+        _float64[0] = a as number;
+        _float64_1[0] = b as number;
         const x = _int32[0] - _int32_1[0];
         if (x !== 0) return x;
         return _int32[1] - _int32_1[1];
     }
 
-    export function compareInArray(xs: ArrayLike<number>, i: number, j: number) {
-        _float64[0] = xs[i];
-        _float64_1[0] = xs[j];
+    export function compareInArray(xs: ArrayLike<IntTuple>, i: number, j: number) {
+        _float64[0] = xs[i] as number;
+        _float64_1[0] = xs[j] as number;
         const x = _int32[0] - _int32_1[0];
         if (x !== 0) return x;
         return _int32[1] - _int32_1[1];
     }
 
-    export function packedHashCode(packed: Packed) {
-        _float64[0] = packed as number;
+    export function hashCode(t: IntTuple) {
+        _float64[0] = t as number;
         return hash2(_int32[0], _int32[1]);
     }
 }
 
-export default IntPair
\ No newline at end of file
+export default IntTuple
\ No newline at end of file
diff --git a/src/structure/collections/multi-set.ts b/src/structure/collections/multi-set.ts
index f7d908d1fcdd1a91cfd88506d3fca302dbb8b581..c3a9d2ef4fe5b859cfaf1735834bde3c6875dcbe 100644
--- a/src/structure/collections/multi-set.ts
+++ b/src/structure/collections/multi-set.ts
@@ -6,126 +6,121 @@
 
 import OrderedSet from './ordered-set'
 import Iterator from './iterator'
-import IntPair from './int-pair'
+import IntTuple from './int-tuple'
 import { sortArray } from './sort'
 import { hash1 } from './hash-functions'
 
-type MultiSetElements = { [id: number]: OrderedSet, offsets: number[], hashCode: number, keys: OrderedSet }
-type MultiSet = number | MultiSetElements
+/** A map-like representation of integer set */
+interface MultiSet { /*'@type': 'int-multi-set'*/ }
 
 namespace MultiSet {
-    export const Empty: MultiSet = { offsets: [0], hashCode: 0, keys: OrderedSet.Empty };
+    export const Empty: MultiSet = { offsets: [0], hashCode: 0, keys: OrderedSet.Empty } as MultiSetElements as any;
 
-    export function create(data: IntPair.Packed | ArrayLike<IntPair.Packed> | IntPair | { [id: number]: OrderedSet }): MultiSet {
+    export function create(data: IntTuple | ArrayLike<IntTuple> | IntTuple | { [id: number]: OrderedSet }): MultiSet {
         if (typeof data === 'number') return data;
-        if (IntPair.is(data)) return IntPair.pack(data);
-        if (isArrayLike(data)) return ofPackedPairs(data);
+        if (IntTuple.is(data)) return IntTuple.pack1(data) as number;
+        if (isArrayLike(data)) return ofTuples(data);
         return ofObject(data as { [id: number]: OrderedSet });
     }
 
     export function keys(set: MultiSet): OrderedSet {
         if (typeof set === 'number') return OrderedSet.ofSingleton(set);
-        return set.keys;
+        return (set as MultiSetElements).keys;
     }
 
-    export function getSetCount(set: MultiSet): number {
+    export function keyCount(set: MultiSet): number {
         if (typeof set === 'number') return 1;
-        return OrderedSet.size(set.keys);
+        return OrderedSet.size((set as MultiSetElements).keys);
     }
 
     export function hasKey(set: MultiSet, key: number): boolean {
-        if (typeof set === 'number') return IntPair.fst(set) === key;
-        return OrderedSet.has(set.keys, key);
+        if (typeof set === 'number') return IntTuple.fst(set) === key;
+        return OrderedSet.has((set as MultiSetElements).keys, key);
     }
 
-    const _hasP = IntPair.zero();
-    export function has(set: MultiSet, pair: number): boolean {
-        if (typeof set === 'number') {
-            return IntPair.areEqual(pair, set);
-        }
-        IntPair.unpack(pair, _hasP);
-        return OrderedSet.has(set.keys, _hasP.fst) ? OrderedSet.has(set[_hasP.fst], _hasP.snd) : false;
+    export function getKey(set: MultiSet, index: number): number {
+        if (typeof set === 'number') return IntTuple.fst(set);
+        return OrderedSet.getAt((set as MultiSetElements).keys, index);
+    }
+
+    export function has(set: MultiSet, t: IntTuple): boolean {
+        if (typeof set === 'number') return IntTuple.areEqual(t, set);
+        IntTuple.unpack(t, _hasP);
+        return OrderedSet.has((set as MultiSetElements).keys, _hasP.fst) ? OrderedSet.has((set as MultiSetElements)[_hasP.fst], _hasP.snd) : false;
     }
+    const _hasP = IntTuple.zero();
 
-    const _gS = IntPair.zero();
-    export function getSetByKey(set: MultiSet, key: number): OrderedSet {
+    export function getByKey(set: MultiSet, key: number): OrderedSet {
         if (typeof set === 'number') {
-            IntPair.unpack(set, _gS);
+            IntTuple.unpack(set, _gS);
             return _gS.fst === key ? OrderedSet.ofSingleton(_gS.snd) : OrderedSet.Empty;
         }
-        return OrderedSet.has(set.keys, key) ? set[key] : OrderedSet.Empty;
+        return OrderedSet.has((set as MultiSetElements).keys, key) ? (set as MultiSetElements)[key] : OrderedSet.Empty;
     }
+    const _gS = IntTuple.zero();
 
-    export function getKey(set: MultiSet, index: number): number {
-        if (typeof set === 'number') return IntPair.fst(set);
-        return OrderedSet.get(set.keys, index);
+    export function getByIndex(set: MultiSet, index: number): OrderedSet {
+        if (typeof set === 'number') return index === 0 ? OrderedSet.ofSingleton(IntTuple.snd(set)) : OrderedSet.Empty;
+        const key = OrderedSet.getAt((set as MultiSetElements).keys, index);
+        return (set as MultiSetElements)[key] || OrderedSet.Empty;
     }
 
-    export function getSetByIndex(set: MultiSet, index: number): OrderedSet {
-        if (typeof set === 'number') return index === 0 ? OrderedSet.ofSingleton(IntPair.snd(set)) : OrderedSet.Empty;
-        const key = OrderedSet.get(set.keys, index);
-        return set[key] || OrderedSet.Empty;
+    export function getAt(set: MultiSet, i: number): IntTuple {
+        if (typeof set === 'number') return set;
+        return getAtE(set as MultiSetElements, i);
     }
 
-    export function get(set: MultiSet, i: number): IntPair.Packed {
-        if (typeof set === 'number') return set;
-        const { offsets, keys } = set;
-        const o = getOffsetIndex(offsets, i);
-        if (o >= offsets.length - 1) return 0;
-        const k = OrderedSet.get(keys, o);
-        const e = OrderedSet.get(set[k], i - offsets[o]);
-        return IntPair.pack1(k, e);
+    export function indexOf(set: MultiSet, t: IntTuple) {
+        if (typeof set === 'number') return IntTuple.areEqual(set, t) ? 0 : -1;
+        return indexOfE(set as MultiSetElements, t);
     }
 
+    /** Number elements in the "child" sets */
     export function size(set: MultiSet) {
-        if (typeof set === 'number') return 0;
-        return set.offsets[set.offsets.length - 1];
+        if (typeof set === 'number') return 1;
+        return (set as MultiSetElements).offsets[(set as MultiSetElements).offsets.length - 1];
     }
 
     export function hashCode(set: MultiSet) {
-        if (typeof set === 'number') return IntPair.packedHashCode(set);
-        if (set.hashCode !== -1) return set.hashCode;
-        return computeHash(set);
+        if (typeof set === 'number') return IntTuple.hashCode(set);
+        if ((set as MultiSetElements).hashCode !== -1) return (set as MultiSetElements).hashCode;
+        return computeHash((set as MultiSetElements));
     }
 
     export function areEqual(a: MultiSet, b: MultiSet): boolean {
-        if (a === b) return true;
         if (typeof a === 'number') {
-            if (typeof b === 'number') return a === b;
+            if (typeof b === 'number') return IntTuple.areEqual(a, b);
             return false;
         }
         if (typeof b === 'number') return false;
-        return areEqualEE(a, b);
+        return areEqualEE(a as MultiSetElements, b as MultiSetElements);
     }
 
     export function areIntersecting(a: MultiSet, b: MultiSet): boolean {
-        if (a === b) return true;
         if (typeof a === 'number') {
-            if (typeof b === 'number') return a === b;
-            return areIntersectingNE(a, b);
+            if (typeof b === 'number') return IntTuple.areEqual(a, b);
+            return areIntersectingNE(a, b as MultiSetElements);
         }
-        if (typeof b === 'number') return areIntersectingNE(b, a);
-        return areIntersectingEE(a, b);
+        if (typeof b === 'number') return areIntersectingNE(b, a as MultiSetElements);
+        return areIntersectingEE(a as MultiSetElements, b as MultiSetElements);
     }
 
     export function intersect(a: MultiSet, b: MultiSet): MultiSet {
-        if (a === b) return a;
         if (typeof a === 'number') {
-            if (typeof b === 'number') return a === b ? a : Empty;
-            return intersectNE(a, b);
+            if (typeof b === 'number') return IntTuple.areEqual(a, b) ? a : Empty;
+            return intersectNE(a, b as MultiSetElements);
         }
-        if (typeof b === 'number') return intersectNE(b, a);
-        return intersectEE(a, b);
+        if (typeof b === 'number') return intersectNE(b, a as MultiSetElements);
+        return intersectEE(a as MultiSetElements, b as MultiSetElements);
     }
 
     export function subtract(a: MultiSet, b: MultiSet): MultiSet {
-        if (a === b) return Empty;
         if (typeof a === 'number') {
-            if (typeof b === 'number') return a === b ? Empty : a;
-            return subtractNE(a, b);
+            if (typeof b === 'number') return IntTuple.areEqual(a, b) ? Empty : a;
+            return subtractNE(a, b as MultiSetElements);
         }
-        if (typeof b === 'number') return subtractEN(a, b);
-        return subtractEE(a, b);
+        if (typeof b === 'number') return subtractEN(a as MultiSetElements, b);
+        return subtractEE(a as MultiSetElements, b as MultiSetElements);
     }
 
     export function union(a: MultiSet, b: MultiSet): MultiSet {
@@ -136,8 +131,8 @@ namespace MultiSet {
         return findUnion(sets);
     }
 
-    class ElementsIterator implements Iterator<IntPair> {
-        private pair = IntPair.zero();
+    class ElementsIterator implements Iterator<IntTuple.Unpacked> {
+        private pair = IntTuple.zero();
 
         private keyCount: number;
         private setIndex = -1;
@@ -156,7 +151,7 @@ namespace MultiSet {
                 if (!this.advance()) return this.pair;
             }
 
-            this.pair.snd = OrderedSet.get(this.currentSet, this.currentIndex++);
+            this.pair.snd = OrderedSet.getAt(this.currentSet, this.currentIndex++);
             return this.pair;
         }
 
@@ -165,7 +160,7 @@ namespace MultiSet {
                 this.done = true;
                 return false;
             }
-            const unit = OrderedSet.get(this.elements.keys, this.setIndex);
+            const unit = OrderedSet.getAt(this.elements.keys, this.setIndex);
             this.pair.fst = unit;
             this.currentSet = this.elements[unit];
             this.currentIndex = 0;
@@ -180,12 +175,14 @@ namespace MultiSet {
         }
     }
 
-    export function values(set: MultiSet): Iterator<IntPair> {
-        if (typeof set === 'number') return Iterator.Value(IntPair.unpack1(set));
-        return new ElementsIterator(set);
+    export function values(set: MultiSet): Iterator<IntTuple.Unpacked> {
+        if (typeof set === 'number') return Iterator.Value(IntTuple.unpack1(set));
+        return new ElementsIterator(set as MultiSetElements);
     }
 }
 
+interface MultiSetElements { [id: number]: OrderedSet, offsets: number[], hashCode: number, keys: OrderedSet }
+
 function isArrayLike(x: any): x is ArrayLike<number> {
     return x && (typeof x.length === 'number' && (x instanceof Array || !!x.buffer));
 }
@@ -199,7 +196,7 @@ function ofObject(data: { [id: number]: OrderedSet }) {
     if (!keys.length) return MultiSet.Empty;
     if (keys.length === 1) {
         const set = data[keys[0]];
-        if (OrderedSet.size(set) === 1) return IntPair.pack1(keys[0], OrderedSet.get(set, 0));
+        if (OrderedSet.size(set) === 1) return IntTuple.pack(keys[0], OrderedSet.getAt(set, 0));
     }
     return ofObject1(keys, data);
 }
@@ -208,7 +205,7 @@ function ofObject1(keys: number[], data: { [id: number]: OrderedSet }) {
     if (keys.length === 1) {
         const k = keys[0];
         const set = data[k];
-        if (OrderedSet.size(set) === 1) return IntPair.pack1(k, OrderedSet.get(set, 0));
+        if (OrderedSet.size(set) === 1) return IntTuple.pack(k, OrderedSet.getAt(set, 0));
     }
     sortArray(keys);
     return _createObjectOrdered(OrderedSet.ofSortedArray(keys), data);
@@ -216,9 +213,9 @@ function ofObject1(keys: number[], data: { [id: number]: OrderedSet }) {
 
 function ofObjectOrdered(keys: OrderedSet, data: { [id: number]: OrderedSet }) {
     if (OrderedSet.size(keys) === 1) {
-        const k = OrderedSet.get(keys, 0);
+        const k = OrderedSet.getAt(keys, 0);
         const set = data[k];
-        if (OrderedSet.size(set) === 1) return IntPair.pack1(k, OrderedSet.get(set, 0));
+        if (OrderedSet.size(set) === 1) return IntTuple.pack(k, OrderedSet.getAt(set, 0));
     }
     return _createObjectOrdered(keys, data);
 }
@@ -229,7 +226,7 @@ function _createObjectOrdered(keys: OrderedSet, data: { [id: number]: OrderedSet
     const offsets = [0];
     let size = 0;
     for (let i = 0, _i = OrderedSet.size(keys); i < _i; i++) {
-        const k = OrderedSet.get(keys, i);
+        const k = OrderedSet.getAt(keys, i);
         const set = data[k];
         ret[k] = set;
         size += OrderedSet.size(set);
@@ -262,12 +259,12 @@ function normalizeArray(xs: number[]) {
     return xs;
 }
 
-function ofPackedPairs(xs: ArrayLike<number>): MultiSet {
+function ofTuples(xs: ArrayLike<IntTuple>): MultiSet {
     if (xs.length === 0) return MultiSet.Empty;
     const sets: { [key: number]: number[] } = Object.create(null);
-    const p = IntPair.zero();
+    const p = IntTuple.zero();
     for (let i = 0, _i = xs.length; i < _i; i++) {
-        IntPair.unpack(xs[i], p);
+        IntTuple.unpack(xs[i], p);
         const set = sets[p.fst];
         if (set) set[set.length] = p.snd;
         else sets[p.fst] = [p.snd];
@@ -297,11 +294,31 @@ function getOffsetIndex(xs: ArrayLike<number>, value: number) {
     return value < xs[min] ? min - 1 : min;
 }
 
+function getAtE(set: MultiSetElements, i: number) {
+    const { offsets, keys } = set;
+    const o = getOffsetIndex(offsets, i);
+    if (o >= offsets.length - 1) return 0;
+    const k = OrderedSet.getAt(keys, o);
+    const e = OrderedSet.getAt(set[k], i - offsets[o]);
+    return IntTuple.pack(k, e);
+}
+
+const _iOE = IntTuple.zero();
+function indexOfE(set: MultiSetElements, t: IntTuple) {
+    IntTuple.unpack(t, _iOE);
+    const { keys } = set;
+    const setIdx = OrderedSet.indexOf(keys, _iOE.fst);
+    if (setIdx < 0) return -1;
+    const o = OrderedSet.indexOf(set[_iOE.fst], _iOE.snd);
+    if (o < 0) return -1;
+    return set.offsets[setIdx] + o;
+}
+
 function computeHash(set: MultiSetElements) {
     const { keys } = set;
     let hash = 23;
     for (let i = 0, _i = OrderedSet.size(keys); i < _i; i++) {
-        const k = OrderedSet.get(keys, i);
+        const k = OrderedSet.getAt(keys, i);
         hash = (31 * hash + k) | 0;
         hash = (31 * hash + OrderedSet.hashCode(set[k])) | 0;
     }
@@ -318,16 +335,15 @@ function areEqualEE(a: MultiSetElements, b: MultiSetElements) {
     const keys = a.keys;
     if (!OrderedSet.areEqual(keys, b.keys)) return false;
     for (let i = 0, _i = OrderedSet.size(keys); i < _i; i++) {
-        const k = OrderedSet.get(keys, i);
+        const k = OrderedSet.getAt(keys, i);
         if (!OrderedSet.areEqual(a[k], b[k])) return false;
     }
     return true;
 }
 
-
-const _aeP = IntPair.zero();
-function areIntersectingNE(a: number, b: MultiSetElements) {
-    IntPair.unpack(a, _aeP);
+const _aeP = IntTuple.zero();
+function areIntersectingNE(a: IntTuple, b: MultiSetElements) {
+    IntTuple.unpack(a, _aeP);
     return OrderedSet.has(b.keys, _aeP.fst) && OrderedSet.has(b[_aeP.fst], _aeP.snd);
 }
 
@@ -337,15 +353,15 @@ function areIntersectingEE(a: MultiSetElements, b: MultiSetElements) {
     if (!OrderedSet.areIntersecting(a.keys, b.keys)) return false;
     const { start, end } = OrderedSet.getIntervalRange(keysA, OrderedSet.min(keysB), OrderedSet.max(keysB));
     for (let i = start; i < end; i++) {
-        const k = OrderedSet.get(keysA, i);
+        const k = OrderedSet.getAt(keysA, i);
         if (OrderedSet.has(keysB, k) && OrderedSet.areIntersecting(a[k], b[k])) return true;
     }
     return false;
 }
 
-const _nP = IntPair.zero();
-function intersectNE(a: number, b: MultiSetElements) {
-    IntPair.unpack(a, _nP);
+const _nP = IntTuple.zero();
+function intersectNE(a: IntTuple, b: MultiSetElements) {
+    IntTuple.unpack(a, _nP);
     return OrderedSet.has(b.keys, _nP.fst) && OrderedSet.has(b[_nP.fst], _nP.snd) ? a : MultiSet.Empty;
 }
 
@@ -358,7 +374,7 @@ function intersectEE(a: MultiSetElements, b: MultiSetElements) {
 
     const keys = [], ret = Object.create(null);
     for (let i = start; i < end; i++) {
-        const k = OrderedSet.get(keysA, i);
+        const k = OrderedSet.getAt(keysA, i);
         if (OrderedSet.has(keysB, k)) {
             const intersection = OrderedSet.intersect(a[k], b[k]);
             if (OrderedSet.size(intersection) > 0) {
@@ -370,16 +386,16 @@ function intersectEE(a: MultiSetElements, b: MultiSetElements) {
     return ofObjectOrdered(OrderedSet.ofSortedArray(keys), ret);
 }
 
-const _sNE = IntPair.zero();
-function subtractNE(a: number, b: MultiSetElements) {
-    IntPair.unpack(a, _sNE);
+const _sNE = IntTuple.zero();
+function subtractNE(a: IntTuple, b: MultiSetElements) {
+    IntTuple.unpack(a, _sNE);
     return OrderedSet.has(b.keys, _sNE.fst) && OrderedSet.has(b[_sNE.fst], _sNE.snd) ? MultiSet.Empty : a;
 }
 
-const _sEN = IntPair.zero();
-function subtractEN(a: MultiSetElements, b: number): MultiSet {
+const _sEN = IntTuple.zero();
+function subtractEN(a: MultiSetElements, b: IntTuple): MultiSet {
     const aKeys =  a.keys;
-    IntPair.unpack(b, _sEN);
+    IntTuple.unpack(b, _sEN);
     if (!OrderedSet.has(aKeys, _sEN.fst) || !OrderedSet.has(a[_sEN.fst], _sEN.snd)) return a;
     const set = a[_sEN.fst];
     if (OrderedSet.size(set) === 1) {
@@ -390,7 +406,7 @@ function subtractEN(a: MultiSetElements, b: number): MultiSet {
 }
 
 function subtractEE(a: MultiSetElements, b: MultiSetElements) {
-    if (a === b) return a;
+    if (a === b) return MultiSet.Empty;
 
     const keysA = a.keys, keysB = b.keys;
     if (!OrderedSet.areIntersecting(a.keys, b.keys)) return MultiSet.Empty;
@@ -398,12 +414,12 @@ function subtractEE(a: MultiSetElements, b: MultiSetElements) {
 
     const keys = [], ret = Object.create(null);
     for (let i = 0; i < start; i++) {
-        const k = OrderedSet.get(keysA, i);
+        const k = OrderedSet.getAt(keysA, i);
         keys[keys.length] = k;
         ret[k] = a[k];
     }
     for (let i = start; i < end; i++) {
-        const k = OrderedSet.get(keysA, i);
+        const k = OrderedSet.getAt(keysA, i);
         if (OrderedSet.has(keysB, k)) {
             const subtraction = OrderedSet.subtract(a[k], b[k]);
             if (OrderedSet.size(subtraction) > 0) {
@@ -416,7 +432,7 @@ function subtractEE(a: MultiSetElements, b: MultiSetElements) {
         }
     }
     for (let i = end, _i = OrderedSet.size(keysA); i < _i; i++) {
-        const k = OrderedSet.get(keysA, i);
+        const k = OrderedSet.getAt(keysA, i);
         keys[keys.length] = k;
         ret[k] = a[k];
     }
@@ -426,7 +442,7 @@ function subtractEE(a: MultiSetElements, b: MultiSetElements) {
 function findUnion(sets: ArrayLike<MultiSet>) {
     if (!sets.length) return MultiSet.Empty;
     if (sets.length === 1) return sets[0];
-    if (sets.length === 2 && sets[0] === sets[1]) return sets[0];
+    if (sets.length === 2 && MultiSet.areEqual(sets[0], sets[1])) return sets[0];
 
     const eCount = { count: 0 };
     const ns = unionN(sets, eCount);
@@ -434,11 +450,11 @@ function findUnion(sets: ArrayLike<MultiSet>) {
     const ret = Object.create(null);
     for (let i = 0, _i = sets.length; i < _i; i++) {
         const s = sets[i];
-        if (typeof s !== 'number') unionInto(ret, s);
+        if (typeof s !== 'number') unionInto(ret, s as MultiSetElements);
     }
     if (MultiSet.size(ns) > 0) {
         if (typeof ns === 'number') unionIntoN(ret, ns);
-        else unionInto(ret, ns);
+        else unionInto(ret, ns as MultiSetElements);
     }
     return ofObject(ret);
 }
@@ -451,29 +467,29 @@ function unionN(sets: ArrayLike<MultiSet>, eCount: { count: number }) {
     }
     eCount.count = countE;
     if (!countN) return MultiSet.Empty;
-    if (countN === sets.length) return ofPackedPairs(sets as ArrayLike<number>);
+    if (countN === sets.length) return ofTuples(sets as ArrayLike<number>);
     const packed = new Float64Array(countN);
     let offset = 0;
     for (let i = 0, _i = sets.length; i < _i; i++) {
         const s = sets[i];
         if (typeof s === 'number') packed[offset++] = s;
     }
-    return ofPackedPairs(packed);
+    return ofTuples(packed);
 }
 
 function unionInto(data: { [key: number]: OrderedSet }, a: MultiSetElements) {
     const keys = a.keys;
     for (let i = 0, _i = OrderedSet.size(keys); i < _i; i++) {
-        const k = OrderedSet.get(keys, i);
+        const k = OrderedSet.getAt(keys, i);
         const set = data[k];
         if (set) data[k] = OrderedSet.union(set, a[k]);
         else data[k] = a[k];
     }
 }
 
-const _uIN = IntPair.zero();
-function unionIntoN(data: { [key: number]: OrderedSet }, a: number) {
-    IntPair.unpack(a, _uIN);
+const _uIN = IntTuple.zero();
+function unionIntoN(data: { [key: number]: OrderedSet }, a: IntTuple) {
+    IntTuple.unpack(a, _uIN);
     const set = data[_uIN.fst];
     if (set) {
         data[_uIN.fst] = OrderedSet.union(set, OrderedSet.ofSingleton(_uIN.snd));
diff --git a/src/structure/collections/ordered-set.ts b/src/structure/collections/ordered-set.ts
index 0aa5ca57eec081fdf755591bc5aba13927b45e50..a1d5e2e2d197593d56c3b4a046551217386cc6af 100644
--- a/src/structure/collections/ordered-set.ts
+++ b/src/structure/collections/ordered-set.ts
@@ -4,135 +4,156 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import IntPair from './int-pair'
+import IntTuple from './int-tuple'
 import { hash3, hash4 } from './hash-functions'
 
-type Nums = ArrayLike<number>
-type OrderedSet = number | Nums
-
 /** An immutable ordered set. */
+interface OrderedSet { '@type': 'int-ordered-set' }
+
 namespace OrderedSet {
-    export function ofSingleton(value: number): OrderedSet { return IntPair.pack1(value, value); }
-    export function ofRange(min: number, max: number): OrderedSet { return max < min ? Empty : IntPair.pack1(min, max); }
+    export const Empty: OrderedSet = IntTuple.pack(0, -1) as any;
+    export function ofSingleton(value: number): OrderedSet { return IntTuple.pack(value, value) as any; }
+    export function ofRange(min: number, max: number): OrderedSet { return max < min ? Empty : IntTuple.pack(min, max) as any; }
     /** It is the responsibility of the caller to ensure the array is sorted and contains unique values. */
-    export function ofSortedArray(xs: Nums): OrderedSet {
+    export function ofSortedArray(xs: SortedArray): OrderedSet {
         if (!xs.length) return Empty;
         // check if the array is just a range
         if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return ofRange(xs[0], xs[xs.length - 1]);
-        return xs;
-    }
-    export const Empty: OrderedSet = IntPair.pack1(0, -1);
-
-    export function size(set: OrderedSet) { return typeof set === 'number' ? sizeR(set) : set.length; }
-    export function has(set: OrderedSet, x: number) { return typeof set === 'number' ? hasR(set, x) : hasA(set, x); }
-    export function indexOf(set: OrderedSet, x: number) { return typeof set === 'number' ? indexOfR(set, x) : indexOfA(set, x); }
-    export function get(set: OrderedSet, i: number) { return typeof set === 'number' ? elementAtR(set, i) : set[i]; }
-    export function min(set: OrderedSet) { return typeof set === 'number' ? minR(set) : set[0]; }
-    export function max(set: OrderedSet) { return typeof set === 'number' ? maxR(set) : set[set.length - 1]; }
-
-    export function hashCode(set: OrderedSet) {
-        // hash of tuple (size, min, max, mid)
-        const s = size(set);
-        if (!s) return 0;
-        if (s > 2) return hash4(s, get(set, 0), get(set, s - 1), get(set, s >> 1));
-        return hash3(s, get(set, 0), get(set, s - 1));
-    }
-    // TODO: possibly add more hash functions to allow for multilevel hashing.
-
-    export function areEqual(a: OrderedSet, b: OrderedSet) {
-        if (typeof a === 'number') {
-            if (typeof b === 'number') return equalRR(a, b);
-            return false;
-        } else if (typeof b === 'number') return false;
-        else if (a === b) return true;
-        return equalAA(a, b);
+        return xs as any;
     }
 
-    export function areIntersecting(a: OrderedSet, b: OrderedSet) {
-        // if at least one is "range", they must now intersect
-        if (typeof a === 'number') {
-            if (typeof b === 'number') return equalRR(a, b) || areRangesIntersecting(a, b);
-            return areRangesIntersecting(a, b);
-        }
-        if (!areRangesIntersecting(a, b)) return false;
-        else if (typeof b === 'number') return false;
-        if (a === b) return true;
-        return areIntersectingAA(a, b);
-    }
+    export const size: (set: OrderedSet) => number = sizeI as any;
+    export const has: (set: OrderedSet, x: number) => boolean = hasI as any;
+    export const indexOf: (set: OrderedSet, x: number) => number = indexOfI as any;
+    export const getAt: (set: OrderedSet, i: number) => number = getAtI as any;
+    export const min: (set: OrderedSet) => number = minI as any;
+    export const max: (set: OrderedSet) => number = maxI as any;
+    export const hashCode: (set: OrderedSet) => number = hashCodeI as any;
 
-    /** Check if the 2nd argument is a subset of the 1st */
-    export function isSubset(set: OrderedSet, toTest: OrderedSet) {
-        if (set === toTest) return true;
-        if (!isRangeSubset(set, toTest)) return false;
-        const testSize = size(toTest);
-        if (typeof set === 'number' || !testSize) return true;
-        if (typeof toTest === 'number') return indexOf(set, maxR(toTest)) - indexOf(set, minR(toTest)) + 1 === testSize;
-        return isSubsetAA(set, toTest);
-    }
+    export const areEqual: BinaryTest = areEqualI as any;
+    export const areIntersecting: BinaryTest = areIntersectingI as any;
+    export const isSubset: BinaryTest = isSubsetI as any;
 
-    export function getInsertionIndex(set: OrderedSet, x: number) {
-        return typeof set === 'number' ? rangeSearchIndex(set, x) : binarySearchIndex(set, x);
-    }
+    export const union: BinaryOp = unionI as any;
+    export const intersect: BinaryOp = intersectI as any;
+    export const subtract: BinaryOp = subtractI as any;
 
-    export function getIntervalRange(set: OrderedSet, min: number, max: number) {
-        const { start, end } = getStartEnd(set, min, max);
-        return { start, end };
-    }
+    export const getInsertionIndex: (set: OrderedSet, x: number) => number = getInsertionIndexI as any;
+    export const getIntervalRange: (set: OrderedSet, min: number, max: number) => { start: number, end: number } = getIntervalRangeI as any;
+}
 
-    export function union(a: OrderedSet, b: OrderedSet) {
-        if (a === b) return a;
-        if (typeof a === 'number') {
-            if (typeof b === 'number') return unionRR(a, b);
-            return unionAR(b, a);
-        } else if (typeof b === 'number') {
-            return unionAR(a, b);
-        } else return unionAA(a, b);
-    }
+export default OrderedSet
 
-    export function intersect(a: OrderedSet, b: OrderedSet) {
-        if (a === b) return a;
-        if (typeof a === 'number') {
-            if (typeof b === 'number') return intersectRR(a, b);
-            return intersectAR(b, a);
-        } else if (typeof b === 'number') {
-            return intersectAR(a, b);
-        } else {
-            if (!areRangesIntersecting(a, b)) return Empty;
-            return intersectAA(a, b);
-        }
+/** Long and painful implementation starts here */
+
+type BinaryTest = (a: OrderedSet, b: OrderedSet) => boolean
+type BinaryOp = (a: OrderedSet, b: OrderedSet) => OrderedSet
+
+type Range = IntTuple
+type SortedArray = ArrayLike<number>
+type OrderedSetImpl = Range | SortedArray
+
+function sizeI(set: OrderedSetImpl) { return typeof set === 'number' ? sizeR(set) : (set as SortedArray).length; }
+function hasI(set: OrderedSetImpl, x: number) { return typeof set === 'number' ? hasR(set, x) : hasA(set as SortedArray, x); }
+function indexOfI(set: OrderedSetImpl, x: number) { return typeof set === 'number' ? indexOfR(set, x) : indexOfA(set as SortedArray, x); }
+function getAtI(set: OrderedSetImpl, i: number) { return typeof set === 'number' ? elementAtR(set, i) : (set as SortedArray)[i]; }
+function minI(set: OrderedSetImpl) { return typeof set === 'number' ? minR(set) : (set as SortedArray)[0]; }
+function maxI(set: OrderedSetImpl) { return typeof set === 'number' ? maxR(set) : (set as SortedArray)[(set as SortedArray).length - 1]; }
+
+function hashCodeI(set: OrderedSetImpl) {
+    // hash of tuple (size, min, max, mid)
+    const s = sizeI(set);
+    if (!s) return 0;
+    if (s > 2) return hash4(s, getAtI(set, 0), getAtI(set, s - 1), getAtI(set, s >> 1));
+    return hash3(s, getAtI(set, 0), getAtI(set, s - 1));
+}
+// TODO: possibly add more hash functions to allow for multilevel hashing.
+
+function areEqualI(a: OrderedSetImpl, b: OrderedSetImpl) {
+    if (typeof a === 'number') {
+        if (typeof b === 'number') return equalRR(a, b);
+        return false;
+    } else if (typeof b === 'number') return false;
+    return equalAA(a as SortedArray, b as SortedArray);
+}
+
+function areIntersectingI(a: OrderedSetImpl, b: OrderedSetImpl) {
+    // if at least one is "range", they must now intersect
+    if (typeof a === 'number') {
+        if (typeof b === 'number') return equalRR(a, b) || areRangesIntersecting(a, b);
+        return areRangesIntersecting(a, b);
     }
+    if (!areRangesIntersecting(a, b)) return false;
+    else if (typeof b === 'number') return false;
+    return areIntersectingAA(a as SortedArray, b as SortedArray);
+}
 
-    export function subtract(a: OrderedSet, b: OrderedSet) {
-        if (a === b) return Empty;
-        if (!areRangesIntersecting(a, b)) return a;
-
-        if (typeof a === 'number') {
-            if (typeof b === 'number') return substractRR(a, b);
-            return subtractRA(a, b);
-        } else if (typeof b === 'number') {
-            return subtractAR(a, b);
-        } else {
-            return subtractAA(a, b);
-        }
+/** Check if the 2nd argument is a subset of the 1st */
+function isSubsetI(set: OrderedSetImpl, toTest: OrderedSetImpl) {
+    if (!isRangeSubset(set, toTest)) return false;
+    const testSize = sizeI(toTest);
+    if (typeof set === 'number' || !testSize) return true;
+    if (typeof toTest === 'number') return indexOfI(set, maxR(toTest)) - indexOfI(set, minR(toTest)) + 1 === testSize;
+    return isSubsetAA(set as SortedArray, toTest as SortedArray);
+}
+
+function getInsertionIndexI(set: OrderedSetImpl, x: number) {
+    return typeof set === 'number' ? rangeSearchIndex(set, x) : binarySearchIndex(set as SortedArray, x);
+}
+
+function getIntervalRangeI(set: OrderedSetImpl, min: number, max: number) {
+    const { start, end } = getStartEnd(set, min, max);
+    return { start, end };
+}
+
+function unionI(a: OrderedSetImpl, b: OrderedSetImpl) {
+    if (typeof a === 'number') {
+        if (typeof b === 'number') return unionRR(a, b);
+        return unionAR(b as SortedArray, a);
+    } else if (typeof b === 'number') {
+        return unionAR(a as SortedArray, b);
+    } else return unionAA(a as SortedArray, b as SortedArray);
+}
+
+function intersectI(a: OrderedSetImpl, b: OrderedSetImpl) {
+    if (typeof a === 'number') {
+        if (typeof b === 'number') return intersectRR(a, b);
+        return intersectAR(b as SortedArray, a);
+    } else if (typeof b === 'number') {
+        return intersectAR(a as SortedArray, b);
+    } else {
+        if (!areRangesIntersecting(a, b)) return OrderedSet.Empty;
+        return intersectAA(a as SortedArray, b as SortedArray);
     }
 }
 
-import S = OrderedSet
+function subtractI(a: OrderedSetImpl, b: OrderedSetImpl) {
+    if (!areRangesIntersecting(a, b)) return a;
 
-const minR = IntPair.fst
-const maxR = IntPair.snd
-const equalRR = IntPair.areEqual
+    if (typeof a === 'number') {
+        if (typeof b === 'number') return substractRR(a, b);
+        return subtractRA(a, b as SortedArray);
+    } else if (typeof b === 'number') {
+        return subtractAR(a as SortedArray, b);
+    } else {
+        return subtractAA(a as SortedArray, b as SortedArray);
+    }
+}
 
-const _eR = IntPair.zero();
-function sizeR(set: number) { IntPair.unpack(set, _eR); return _eR.snd - _eR.fst + 1; }
-function hasR(set: number, x: number) { IntPair.unpack(set, _eR); return x >= _eR.fst && x <= _eR.snd; }
-function indexOfR(set: number, x: number) { IntPair.unpack(set, _eR); return x >= _eR.fst && x <= _eR.snd ? x - _eR.fst : -1; }
-function elementAtR(set: number, i: number) { return IntPair.fst(set) + i; }
+const minR = IntTuple.fst
+const maxR = IntTuple.snd
+const equalRR = IntTuple.areEqual
 
-function hasA(set: Nums, x: number) { return x >= set[0] && x <= set[set.length - 1] && binarySearch(set, x) >= 0; }
-function indexOfA(set: Nums, x: number) { return x >= set[0] && x <= set[set.length - 1] ? binarySearch(set, x) : -1; }
+const _eR = IntTuple.zero();
+function sizeR(set: Range) { IntTuple.unpack(set, _eR); return _eR.snd - _eR.fst + 1; }
+function hasR(set: Range, x: number) { IntTuple.unpack(set, _eR); return x >= _eR.fst && x <= _eR.snd; }
+function indexOfR(set: Range, x: number) { IntTuple.unpack(set, _eR); return x >= _eR.fst && x <= _eR.snd ? x - _eR.fst : -1; }
+function elementAtR(set: Range, i: number) { return IntTuple.fst(set) + i; }
 
-function binarySearch(xs: Nums, value: number) {
+function hasA(set: SortedArray, x: number) { return x >= set[0] && x <= set[set.length - 1] && binarySearch(set, x) >= 0; }
+function indexOfA(set: SortedArray, x: number) { return x >= set[0] && x <= set[set.length - 1] ? binarySearch(set, x) : -1; }
+
+function binarySearch(xs: SortedArray, value: number) {
     let min = 0, max = xs.length - 1;
     while (min <= max) {
         if (min + 11 > max) {
@@ -151,7 +172,7 @@ function binarySearch(xs: Nums, value: number) {
     return -1;
 }
 
-function binarySearchIndex(xs: Nums, value: number) {
+function binarySearchIndex(xs: SortedArray, value: number) {
     let min = 0, max = xs.length - 1;
     while (min < max) {
         const mid = (min + max) >> 1;
@@ -164,16 +185,16 @@ function binarySearchIndex(xs: Nums, value: number) {
     return xs[min] >= value ? min : min + 1;
 }
 
-const _rsiR = IntPair.zero();
-function rangeSearchIndex(r: number, value: number) {
-    IntPair.unpack(r, _rsiR);
+const _rsiR = IntTuple.zero();
+function rangeSearchIndex(r: Range, value: number) {
+    IntTuple.unpack(r, _rsiR);
     if (value < _rsiR.fst) return 0;
     if (value > _rsiR.snd) return _rsiR.snd - _rsiR.fst + 1;
     return value - _rsiR.fst;
 }
 
 const _maxIntRangeRet = { i: 0, j: 0, endA: 0, endB: 0 };
-function getMaxIntersectionRange(xs: Nums, ys: Nums) {
+function getMaxIntersectionRange(xs: SortedArray, ys: SortedArray) {
     const la = xs.length - 1, lb = ys.length - 1;
     _maxIntRangeRet.i = binarySearchIndex(xs, ys[0]);
     _maxIntRangeRet.j = binarySearchIndex(ys, xs[0]);
@@ -184,15 +205,16 @@ function getMaxIntersectionRange(xs: Nums, ys: Nums) {
 
 const _startEndRet = { start: 0, end: 0 };
 
-function getStartEnd(set: OrderedSet, min: number, max: number) {
-    _startEndRet.start = S.getInsertionIndex(set, min);
-    let end = S.getInsertionIndex(set, max);
-    if (end < S.size(set) && S.get(set, end) === max) end++;
+function getStartEnd(set: OrderedSetImpl, min: number, max: number) {
+    _startEndRet.start = getInsertionIndexI(set, min);
+    let end = getInsertionIndexI(set, max);
+    if (end < sizeI(set) && getAtI(set, end) === max) end++;
     _startEndRet.end = end;
     return _startEndRet;
 }
 
-function equalAA(a: Nums, b: Nums) {
+function equalAA(a: SortedArray, b: SortedArray) {
+    if (a === b) return true;
     let size = a.length;
     if (a.length !== b.length || a[0] !== b[0] || a[size - 1] !== b[size - 1]) return false;
     for (let i = 0; i < size; i++) {
@@ -201,7 +223,9 @@ function equalAA(a: Nums, b: Nums) {
     return true;
 }
 
-function areIntersectingAA(xs: Nums, ys: Nums) {
+function areIntersectingAA(xs: SortedArray, ys: SortedArray) {
+    if (xs === ys) return true;
+
     let { i, j, endA, endB } = getMaxIntersectionRange(xs, ys);
     while (i <= endA && j <= endB) {
         const x = xs[i], y = ys[j];
@@ -212,7 +236,9 @@ function areIntersectingAA(xs: Nums, ys: Nums) {
     return false;
 }
 
-function isSubsetAA(xs: Nums, ys: Nums) {
+function isSubsetAA(xs: SortedArray, ys: SortedArray) {
+    if (xs === ys) return true;
+
     const lenB = ys.length;
     let { i, j, endA, endB } = getMaxIntersectionRange(xs, ys);
     // the 2nd array must be able to advance by at least lenB elements
@@ -228,44 +254,45 @@ function isSubsetAA(xs: Nums, ys: Nums) {
     return equal === lenB;
 }
 
-function areRangesIntersecting(a: OrderedSet, b: OrderedSet) {
-    return S.size(a) > 0 && S.size(b) > 0 && S.max(a) >= S.min(b) && S.min(a) <= S.max(b);
+function areRangesIntersecting(a: OrderedSetImpl, b: OrderedSetImpl) {
+    return sizeI(a) > 0 && sizeI(b) > 0 && maxI(a) >= minI(b) && minI(a) <= maxI(b);
 }
 
-function isRangeSubset(a: OrderedSet, b: OrderedSet) {
-    if (!S.size(a)) return S.size(b) === 0;
-    if (!S.size(b)) return true;
-    return S.min(a) <= S.min(b) && S.max(a) >= S.max(b);
+function isRangeSubset(a: OrderedSetImpl, b: OrderedSetImpl) {
+    if (!sizeI(a)) return sizeI(b) === 0;
+    if (!sizeI(b)) return true;
+    return minI(a) <= minI(b) && maxI(a) >= maxI(b);
 }
 
-function unionRR(a: number, b: number) {
-    const sizeA = S.size(a), sizeB = S.size(b);
+function unionRR(a: Range, b: Range) {
+    if (IntTuple.areEqual(a, b)) return a;
+
+    const sizeA = sizeR(a), sizeB = sizeR(b);
     if (!sizeA) return b;
     if (!sizeB) return a;
     const minA = minR(a), minB = minR(b);
-    if (areRangesIntersecting(a, b)) return S.ofRange(Math.min(minA, minB), Math.max(maxR(a), maxR(b)));
+    if (areRangesIntersecting(a as number, b as number)) return OrderedSet.ofRange(Math.min(minA, minB), Math.max(maxR(a), maxR(b)));
     let lSize, lMin, rSize, rMin;
     if (minR(a) < minR(b)) { lSize = sizeA; lMin = minA; rSize = sizeB; rMin = minB; }
     else { lSize = sizeB; lMin = minB; rSize = sizeA; rMin = minA; }
     const arr = new Int32Array(sizeA + sizeB);
     for (let i = 0; i < lSize; i++) arr[i] = i + lMin;
     for (let i = 0; i < rSize; i++) arr[i + lSize] = i + rMin;
-    return S.ofSortedArray(arr);
+    return OrderedSet.ofSortedArray(arr);
 }
 
-const _uAR = IntPair.zero();
-function unionAR(a: Nums, b: number) {
-    const bSize = S.size(b);
+const _uAR = IntTuple.zero();
+function unionAR(a: SortedArray, b: Range) {
+    const bSize = sizeI(b);
     if (!bSize) return a;
     // is the array fully contained in the range?
     if (isRangeSubset(b, a)) return b;
 
-    IntPair.unpack(b, _uAR);
+    IntTuple.unpack(b, _uAR);
     const min = _uAR.fst, max = _uAR.snd;
     const { start, end } = getStartEnd(a, min, max);
 
-    const size = start + (a.length - end) + bSize;
-    const indices = new Int32Array(size);
+    const indices = new Int32Array(start + (a.length - end) + bSize);
     let offset = 0;
     for (let i = 0; i < start; i++) indices[offset++] = a[i];
     for (let i = min; i <= max; i++) indices[offset++] = i;
@@ -274,7 +301,9 @@ function unionAR(a: Nums, b: number) {
     return OrderedSet.ofSortedArray(indices);
 }
 
-function unionAA(a: Nums, b: Nums) {
+function unionAA(a: SortedArray, b: SortedArray) {
+    if (a === b) return a;
+
     const lenA = a.length, lenB = b.length;
 
     let { i: sI, j: sJ, endA, endB } = getMaxIntersectionRange(a, b);
@@ -321,20 +350,21 @@ function unionAA(a: Nums, b: Nums) {
     return OrderedSet.ofSortedArray(indices);
 }
 
-const _iRA = IntPair.zero(), _iRB = IntPair.zero();
-function intersectRR(a: number, b: number) {
+const _iRA = IntTuple.zero(), _iRB = IntTuple.zero();
+function intersectRR(a: Range, b: Range) {
     if (!areRangesIntersecting(a, b)) return OrderedSet.Empty;
+    if (IntTuple.areEqual(a, b)) return a;
 
-    IntPair.unpack(a, _iRA);
-    IntPair.unpack(b, _iRB);
+    IntTuple.unpack(a, _iRA);
+    IntTuple.unpack(b, _iRB);
     return OrderedSet.ofRange(Math.max(_iRA.fst, _iRB.fst), Math.min(_iRA.snd, _iRB.snd));
 }
 
-const _iAR = IntPair.zero();
-function intersectAR(a: Nums, r: number) {
-    if (!S.size(r)) return OrderedSet.Empty;
+const _iAR = IntTuple.zero();
+function intersectAR(a: SortedArray, r: Range) {
+    if (!sizeI(r)) return OrderedSet.Empty;
 
-    IntPair.unpack(r, _iAR);
+    IntTuple.unpack(r, _iAR);
     const { start, end } = getStartEnd(a, _iAR.fst, _iAR.snd);
     const resultSize = end - start;
     if (!resultSize) return OrderedSet.Empty;
@@ -347,7 +377,9 @@ function intersectAR(a: Nums, r: number) {
     return OrderedSet.ofSortedArray(indices);
 }
 
-function intersectAA(xs: Nums, ys: Nums) {
+function intersectAA(xs: SortedArray, ys: SortedArray) {
+    if (xs === ys) return xs;
+
     let { i: sI, j: sJ, endA, endB } = getMaxIntersectionRange(xs, ys);
     let i = sI, j = sJ;
     let resultSize = 0;
@@ -374,10 +406,12 @@ function intersectAA(xs: Nums, ys: Nums) {
     return OrderedSet.ofSortedArray(indices);
 }
 
-const _sRA = IntPair.zero(), _sRB = IntPair.zero();
-function substractRR(a: number, b: number) {
-    IntPair.unpack(a, _sRA);
-    IntPair.unpack(b, _sRB);
+const _sRA = IntTuple.zero(), _sRB = IntTuple.zero();
+function substractRR(a: Range, b: Range) {
+    if (IntTuple.areEqual(a, b)) return OrderedSet.Empty;
+
+    IntTuple.unpack(a, _sRA);
+    IntTuple.unpack(b, _sRB);
 
     if (_sRA.snd < _sRA.fst || _sRB.snd < _sRB.fst) return a;
     // is A subset of B? ==> Empty
@@ -397,9 +431,11 @@ function substractRR(a: number, b: number) {
     return OrderedSet.ofRange(_sRB.snd + 1, _sRA.snd);
 }
 
-const _sAR = IntPair.zero();
-function subtractAR(a: Nums, r: number) {
-    IntPair.unpack(r, _sAR);
+const _sAR = IntTuple.zero();
+function subtractAR(a: SortedArray, b: Range) {
+    IntTuple.unpack(b, _sAR);
+
+    // is empty?
     if (_sAR.snd < _sAR.fst) return a;
 
     const min = _sAR.fst, max = _sAR.snd;
@@ -413,10 +449,12 @@ function subtractAR(a: Nums, r: number) {
     return OrderedSet.ofSortedArray(ret);
 }
 
-const _sAR1 = IntPair.zero();
-function subtractRA(r: number, b: Nums) {
-    IntPair.unpack(r, _sAR1);
-    if (_sAR1.snd < _sAR1.fst) return r;
+const _sAR1 = IntTuple.zero();
+function subtractRA(a: Range, b: SortedArray) {
+    IntTuple.unpack(a, _sAR1);
+
+    // is empty?
+    if (_sAR1.snd < _sAR1.fst) return a;
 
     const min = _sAR1.fst, max = _sAR1.snd;
     const rSize = max - min + 1;
@@ -436,7 +474,9 @@ function subtractRA(r: number, b: Nums) {
     return OrderedSet.ofSortedArray(ret);
 }
 
-function subtractAA(a: Nums, b: Nums) {
+function subtractAA(a: SortedArray, b: SortedArray) {
+    if (a === b) return OrderedSet.Empty;
+
     const lenA = a.length;
 
     let { i: sI, j: sJ, endA, endB } = getMaxIntersectionRange(a, b);
@@ -471,6 +511,4 @@ function subtractAA(a: Nums, b: Nums) {
     for (; i < lenA; i++) indices[offset++] = a[i];
 
     return OrderedSet.ofSortedArray(indices);
-}
-
-export default OrderedSet
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/structure/spec/collections.spec.ts b/src/structure/spec/collections.spec.ts
index 60978930d7aa649aad87b82e493d20dc2e6e45f9..13ad53905b0dc4def2dee5d3516fe4de2f211fde 100644
--- a/src/structure/spec/collections.spec.ts
+++ b/src/structure/spec/collections.spec.ts
@@ -5,7 +5,7 @@
  */
 
 import Iterator from '../collections/iterator'
-import IntPair from '../collections/int-pair'
+import IntTuple from '../collections/int-tuple'
 import * as Sort from '../collections/sort'
 import OrderedSet from '../collections/ordered-set'
 import LinkedIndex from '../collections/linked-index'
@@ -33,11 +33,11 @@ describe('basic iterators', () => {
 
 describe('int pair', () => {
     it('works', () => {
-        const p = IntPair.zero();
+        const p = IntTuple.zero();
         for (let i = 0; i < 10; i++) {
             for (let j = -10; j < 5; j++) {
-                const t = IntPair.pack1(i, j);
-                IntPair.unpack(t, p);
+                const t = IntTuple.pack(i, j);
+                IntTuple.unpack(t, p);
                 expect(p.fst).toBe(i);
                 expect(p.snd).toBe(j);
             }
@@ -131,7 +131,7 @@ describe('qsort-dual array', () => {
 describe('ordered set', () => {
     function ordSetToArray(set: OrderedSet) {
         const ret = [];
-        for (let i = 0, _i = OrderedSet.size(set); i < _i; i++) ret.push(OrderedSet.get(set, i));
+        for (let i = 0, _i = OrderedSet.size(set); i < _i; i++) ret.push(OrderedSet.getAt(set, i));
         return ret;
     }
 
@@ -317,13 +317,13 @@ describe('linked-index', () => {
 });
 
 describe('multiset', () => {
-    const p = (i: number, j: number) => IntPair.create(i, j);
-    const r = (i: number, j: number) => IntPair.pack1(i, j);
+    const p = (i: number, j: number) => IntTuple.create(i, j);
+    const r = (i: number, j: number) => IntTuple.pack(i, j);
 
-    function setToPairs(set: MultiSet): IntPair[] {
+    function setToPairs(set: MultiSet): ArrayLike<IntTuple.Unpacked> {
         const ret = [];
         const it = MultiSet.values(set);
-        for (let v = it.move(); !it.done; v = it.move()) ret[ret.length] = IntPair.create(v.fst, v.snd);
+        for (let v = it.move(); !it.done; v = it.move()) ret[ret.length] = IntTuple.create(v.fst, v.snd);
         return ret;
     }
 
@@ -332,7 +332,8 @@ describe('multiset', () => {
         expect(setToPairs(set)).toEqual([p(10, 11)]);
         expect(MultiSet.has(set, r(10, 11))).toBe(true);
         expect(MultiSet.has(set, r(11, 11))).toBe(false);
-        expect(MultiSet.get(set, 0)).toBe(r(10, 11));
+        expect(MultiSet.getAt(set, 0)).toBe(r(10, 11));
+        expect(MultiSet.size(set)).toBe(1);
     });
 
     it('singleton number', () => {
@@ -352,12 +353,12 @@ describe('multiset', () => {
         expect(MultiSet.has(set, r(3, 0))).toBe(true);
         expect(MultiSet.has(set, r(1, 7))).toBe(true);
         for (let i = 0; i < MultiSet.size(set); i++) {
-            expect(MultiSet.get(set, i)).toBe(IntPair.pack(ret[i]));
+            expect(MultiSet.getAt(set, i)).toBe(IntTuple.pack1(ret[i]));
         }
     });
 
-    it('element at', () => {
-        const control = [];
+    it('element at / index of', () => {
+        const control: IntTuple[] = [];
         const sets = Object.create(null);
         for (let i = 1; i < 10; i++) {
             const set = [];
@@ -369,7 +370,11 @@ describe('multiset', () => {
         }
         const ms = MultiSet.create(sets);
         for (let i = 0; i < control.length; i++) {
-            expect(IntPair.areEqual(MultiSet.get(ms, i), control[i])).toBe(true);
+            expect(IntTuple.areEqual(MultiSet.getAt(ms, i), control[i])).toBe(true);
+        }
+
+        for (let i = 0; i < control.length; i++) {
+            expect(MultiSet.indexOf(ms, control[i])).toBe(i);
         }
     });