diff --git a/src/mol-base/_spec/collections.spec.ts b/src/mol-base/_spec/collections.spec.ts
index 4b93ce45a8476b9f82ff5a7b7e5157e5876f7349..89e956cd8266b505cb6dae62471818c3d9b3834c 100644
--- a/src/mol-base/_spec/collections.spec.ts
+++ b/src/mol-base/_spec/collections.spec.ts
@@ -10,6 +10,8 @@ import * as Sort from '../collections/sort'
 import OrderedSet from '../collections/ordered-set'
 import LinkedIndex from '../collections/linked-index'
 import EquivalenceClasses from '../collections/equivalence-classes'
+import Interval from '../collections/interval'
+import SortedArray from '../collections/sorted-array'
 
 function iteratorToArray<T>(it: Iterator<T>): T[] {
     const ret = [];
@@ -125,6 +127,114 @@ describe('qsort-dual array', () => {
     test('shuffled', data, true);
 })
 
+describe('interval', () => {
+    function testI(name: string, a: Interval, b: Interval) {
+        it(name, () => expect(Interval.areEqual(a, b)).toBe(true));
+    }
+
+    function test(name: string, a: any, b: any) {
+        it(name, () => expect(a).toEqual(b));
+    }
+
+    const e = Interval.Empty;
+    const r05 = Interval.ofRange(0, 5);
+    const se05 = Interval.ofBounds(0, 5);
+
+    test('size', Interval.size(e), 0);
+    test('size', Interval.size(r05), 6);
+    test('size', Interval.size(se05), 5);
+
+    test('min/max', [Interval.min(e), Interval.max(e)], [0, -1]);
+    test('min/max', [Interval.min(r05), Interval.max(r05)], [0, 5]);
+    test('min/max', [Interval.min(se05), Interval.max(se05)], [0, 4]);
+
+    test('start/end', [Interval.start(e), Interval.end(e)], [0, 0]);
+    test('start/end', [Interval.start(r05), Interval.end(r05)], [0, 6]);
+    test('start/end', [Interval.start(se05), Interval.end(se05)], [0, 5]);
+
+    test('has', Interval.has(e, 5), false);
+    test('has', Interval.has(r05, 5), true);
+    test('has', Interval.has(r05, 6), false);
+    test('has', Interval.has(r05, -1), false);
+    test('has', Interval.has(se05, 5), false);
+    test('has', Interval.has(se05, 4), true);
+
+    test('indexOf', Interval.indexOf(e, 5), -1);
+    test('indexOf', Interval.indexOf(r05, 5), 5);
+    test('indexOf', Interval.indexOf(r05, 6), -1);
+
+    test('getAt', Interval.getAt(r05, 5), 5);
+
+    test('areEqual', Interval.areEqual(r05, se05), false);
+    test('areIntersecting1', Interval.areIntersecting(r05, se05), true);
+    test('areIntersecting2', Interval.areIntersecting(r05, e), false);
+    test('areIntersecting3', Interval.areIntersecting(e, r05), false);
+    test('areIntersecting4', Interval.areIntersecting(e, e), true);
+
+    test('areIntersecting5', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(-4, 3)), true);
+    test('areIntersecting6', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(-4, -3)), false);
+    test('areIntersecting7', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(1, 2)), true);
+    test('areIntersecting8', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(3, 6)), true);
+
+    test('isSubInterval', Interval.isSubInterval(Interval.ofRange(0, 5), Interval.ofRange(3, 6)), false);
+    test('isSubInterval', Interval.isSubInterval(Interval.ofRange(0, 5), Interval.ofRange(3, 5)), true);
+
+    testI('intersect', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(-4, 3)), Interval.ofRange(0, 3));
+    testI('intersect1', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(1, 3)), Interval.ofRange(1, 3));
+    testI('intersect2', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(3, 5)), Interval.ofRange(3, 5));
+    testI('intersect3', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(-4, -3)), Interval.Empty);
+
+    test('predIndex1', Interval.findPredecessorIndex(r05, 5), 5);
+    test('predIndex2', Interval.findPredecessorIndex(r05, -1), 0);
+    test('predIndex3', Interval.findPredecessorIndex(r05, 6), 6);
+    test('predIndexInt', Interval.findPredecessorIndexInInterval(r05, 0, Interval.ofRange(2, 3)), 2);
+    test('predIndexInt1', Interval.findPredecessorIndexInInterval(r05, 4, Interval.ofRange(2, 3)), 4);
+
+    testI('findRange', Interval.findRange(r05, 2, 3), Interval.ofRange(2, 3));
+});
+
+describe('sortedArray', () => {
+    function testI(name: string, a: Interval, b: Interval) {
+        it(name, () => expect(Interval.areEqual(a, b)).toBe(true));
+    }
+
+    function test(name: string, a: any, b: any) {
+        it(name, () => expect(a).toEqual(b));
+    }
+
+    const a1234 = SortedArray.ofSortedArray([1, 2, 3, 4]);
+    const a2468 = SortedArray.ofSortedArray([2, 4, 6, 8]);
+
+    test('size', SortedArray.size(a1234), 4);
+
+    test('min/max', [SortedArray.min(a1234), SortedArray.max(a1234)], [1, 4]);
+    test('start/end', [SortedArray.start(a1234), SortedArray.end(a1234)], [1, 5]);
+
+    test('has', SortedArray.has(a1234, 5), false);
+    test('has', SortedArray.has(a1234, 4), true);
+
+    it('has-all', () => {
+        for (let i = 1; i <= 4; i++) expect(SortedArray.has(a1234, i)).toBe(true);
+    });
+
+    test('indexOf', SortedArray.indexOf(a2468, 5), -1);
+    test('indexOf', SortedArray.indexOf(a2468, 2), 0);
+
+    test('getAt', SortedArray.getAt(a2468, 1), 4);
+
+    test('areEqual', SortedArray.areEqual(a2468, a2468), true);
+    test('areEqual1', SortedArray.areEqual(a2468, SortedArray.create([4, 2, 8, 6])), true);
+    test('areEqual2', SortedArray.areEqual(a1234, a2468), false);
+
+    test('predIndex1', SortedArray.findPredecessorIndex(a1234, 5), 4);
+    test('predIndex2', SortedArray.findPredecessorIndex(a1234, 2), 1);
+    test('predIndex3', SortedArray.findPredecessorIndex(a2468, 4), 1);
+    test('predIndex4', SortedArray.findPredecessorIndex(a2468, 3), 1);
+    test('predIndexInt', SortedArray.findPredecessorIndexInInterval(a1234, 0, Interval.ofRange(2, 3)), 2);
+
+    testI('findRange', SortedArray.findRange(a2468, 2, 4), Interval.ofRange(0, 1));
+});
+
 describe('ordered set', () => {
     function ordSetToArray(set: OrderedSet) {
         const ret = [];
diff --git a/src/mol-base/collections/impl/interval.ts b/src/mol-base/collections/impl/interval.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0290c22915b6942727b2d5cb32b487aec261789
--- /dev/null
+++ b/src/mol-base/collections/impl/interval.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import IntTuple from '../int-tuple'
+
+export const Empty = IntTuple.Zero;
+export function ofRange(min: number, max: number) { return max < min ? Empty : IntTuple.create(min, max + 1); }
+export function ofBounds(min: number, max: number) { return max <= min ? Empty : IntTuple.create(min, max); }
+export const is = IntTuple.is;
+
+export const start = IntTuple.fst;
+export const end = IntTuple.snd;
+export const min = IntTuple.fst;
+export function max(i: IntTuple) { return IntTuple.snd(i) - 1; }
+export function size(i: IntTuple) { return IntTuple.snd(i) - IntTuple.fst(i); }
+export const hashCode = IntTuple.hashCode;
+
+export function has(int: IntTuple, v: number) { return IntTuple.fst(int) <= v && v < IntTuple.snd(int); }
+export function indexOf(int: IntTuple, x: number) { const m = start(int); return x >= m && x < end(int) ? x - m : -1; }
+export function getAt(int: IntTuple, i: number) { return IntTuple.fst(int) + i; }
+
+export const areEqual = IntTuple.areEqual;
+export function areIntersecting(a: IntTuple, b: IntTuple) {
+    const sa = size(a), sb = size(b);
+    if (sa === 0 && sb === 0) return true;
+    return sa > 0 && sb > 0 && max(a) >= min(b) && min(a) <= max(b);
+}
+export function isSubInterval(a: IntTuple, b: IntTuple) {
+    if (!size(a)) return size(b) === 0;
+    if (!size(b)) return true;
+    return start(a) <= start(b) && end(a) >= end(b);
+}
+
+export function findPredecessorIndex(int: IntTuple, v: number) {
+    const s = start(int);
+    if (v <= s) return 0;
+    const e = end(int);
+    if (v >= e) return e - s;
+    return v - s;
+}
+
+export function findPredecessorIndexInInterval(int: IntTuple, x: number, bounds: IntTuple) {
+    const ret = findPredecessorIndex(int, x);
+    const s = start(bounds), e = end(bounds);
+    return ret <= s ? s : ret >= e ? e : ret;
+}
+
+export function findRange(int: IntTuple, min: number, max: number) {
+    return ofBounds(findPredecessorIndex(int, min), findPredecessorIndex(int, max + 1));
+}
+
+export function intersect(a: IntTuple, b: IntTuple) {
+    if (!areIntersecting(a, b)) return Empty;
+    return ofBounds(Math.max(start(a), start(b)), Math.min(end(a), end(b)));
+}
\ No newline at end of file
diff --git a/src/mol-base/collections/impl/sorted-array.ts b/src/mol-base/collections/impl/sorted-array.ts
new file mode 100644
index 0000000000000000000000000000000000000000..134b1dbf3dd05afafec623a76fadf887e11059e0
--- /dev/null
+++ b/src/mol-base/collections/impl/sorted-array.ts
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { sortArray } from '../sort'
+import { hash3, hash4 } from '../hash-functions'
+import Interval from '../interval'
+
+type Nums = ArrayLike<number>
+
+export function ofSortedArray(xs: Nums) {
+    if (xs.length < 1) throw new Error('Sorted arrays must be non-empty.');
+    return xs;
+}
+export function ofUnsortedArray(xs: Nums) { sortArray(xs); return xs; }
+export function is(xs: any): xs is Nums { return xs && (xs instanceof Array || !!xs.buffer); }
+
+export function start(xs: Nums) { return xs[0]; }
+export function end(xs: Nums) { return xs[xs.length - 1] + 1;  }
+export function min(xs: Nums) { return xs[0]; }
+export function max(xs: Nums) { return xs[xs.length - 1]; }
+export function size(xs: Nums) { return xs.length; }
+export function hashCode(xs: Nums) {
+    // hash of tuple (size, min, max, mid)
+    const s = xs.length;
+    if (!s) return 0;
+    if (s > 2) return hash4(s, xs[0], xs[s - 1], xs[s << 1]);
+    return hash3(s, xs[0], xs[s - 1]);
+}
+
+export function indexOf(xs: Nums, v: number) {
+    const l = xs.length;
+    return l === 0 ? -1 : xs[0] <= v && v <= xs[l - 1] ? binarySearchRange(xs, v, 0, l) : -1;
+}
+export function has(xs: Nums, v: number) { return indexOf(xs, v) >= 0; }
+
+export function getAt(xs: Nums, i: number) { return xs[i]; }
+
+export function areEqual(a: Nums, b: Nums) {
+    if (a === b) return true;
+    const aSize = a.length;
+    if (aSize !== b.length || a[0] !== b[0] || a[aSize - 1] !== b[aSize - 1]) return false;
+    for (let i = 0; i < aSize; i++) {
+        if (a[i] !== b[i]) return false;
+    }
+    return true;
+}
+
+export function findPredecessorIndex(xs: Nums, v: number) {
+    const len = xs.length;
+    if (v <= xs[0]) return 0;
+    if (v > xs[len - 1]) return len;
+    return binarySearchPredIndexRange(xs, v, 0, len);
+}
+
+export function findPredecessorIndexInInterval(xs: Nums, v: number, bounds: Interval) {
+    const s = Interval.start(bounds), e = Interval.end(bounds);
+    if (v <= xs[s]) return s;
+    if (e > s && v > xs[e - 1]) return e;
+    return binarySearchPredIndexRange(xs, v, s, e);
+}
+
+export function findRange(xs: Nums, min: number, max: number) {
+    return Interval.ofBounds(findPredecessorIndex(xs, min), findPredecessorIndex(xs, max + 1));
+}
+
+function binarySearchRange(xs: Nums, value: number, start: number, end: number) {
+    let min = start, max = end - 1;
+    while (min <= max) {
+        if (min + 11 > max) {
+            for (let i = min; i <= max; i++) {
+                if (value === xs[i]) return i;
+            }
+            return -1;
+        }
+
+        const mid = (min + max) >> 1;
+        const v = xs[mid];
+        if (value < v) max = mid - 1;
+        else if (value > v) min = mid + 1;
+        else return mid;
+    }
+    return -1;
+}
+
+function binarySearchPredIndexRange(xs: Nums, value: number, start: number, end: number) {
+    let min = start, max = end - 1;
+    while (min < max) {
+        const mid = (min + max) >> 1;
+        const v = xs[mid];
+        if (value < v) max = mid - 1;
+        else if (value > v) min = mid + 1;
+        else return mid;
+    }
+    if (min > max) return max + 1;
+    return xs[min] >= value ? min : min + 1;
+}
\ No newline at end of file
diff --git a/src/mol-base/collections/interval.ts b/src/mol-base/collections/interval.ts
index 722d48d395b56036bf5c21ff70c08bafdcbdb555..5736efc0d973341eb2ad3eca6818f613f770b1e6 100644
--- a/src/mol-base/collections/interval.ts
+++ b/src/mol-base/collections/interval.ts
@@ -4,44 +4,39 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import IntTuple from './int-tuple'
+import * as Impl from './impl/interval'
 
-/** Closed/Open inteval [a, b) (iterate as a <= i < b) */
 namespace Interval {
     export const Empty: Interval = Impl.Empty as any;
 
-    // export const create: (min: number, max: number) => Interval = Impl.create as any;
+    /** Create interval [min, max] */
+    export const ofRange: (min: number, max: number) => Interval = Impl.ofRange as any;
+    /** Create interval [min, max) */
+    export const ofBounds: (start: number, end: number) => Interval = Impl.ofBounds as any;
+    export const is: (v: any) => v is Interval = Impl.is as any;
 
-    // export const indexOf: (set: Interval, x: number) => number = Impl.indexOf as any;
-    // export const getAt: (set: Interval, i: number) => number = Impl.getAt as any;
+    export const has: (interval: Interval, x: number) => boolean = Impl.has as any;
+    export const indexOf: (interval: Interval, x: number) => number = Impl.indexOf as any;
+    export const getAt: (interval: Interval, i: number) => number = Impl.getAt as any;
 
-    // export const start: (set: Interval) => number = Impl.start as any;
-    // export const end: (set: Interval) => number = Impl.end as any;
+    export const start: (interval: Interval) => number = Impl.start as any;
+    export const end: (interval: Interval) => number = Impl.end as any;
+    export const min: (interval: Interval) => number = Impl.min as any;
+    export const max: (interval: Interval) => number = Impl.max as any;
+    export const size: (interval: Interval) => number = Impl.size as any;
+    export const hashCode: (interval: Interval) => number = Impl.hashCode as any;
 
-    // export const min: (set: Interval) => number = Impl.min as any;
-    // export const max: (set: Interval) => number = Impl.max as any;
-    // export const size: (set: Interval) => number = Impl.size as any;
-    // export const hashCode: (set: Interval) => number = Impl.hashCode as any;
+    export const areEqual: (a: Interval, b: Interval) => boolean = Impl.areEqual as any;
+    export const areIntersecting: (a: Interval, b: Interval) => boolean = Impl.areIntersecting as any;
+    export const isSubInterval: (a: Interval, b: Interval) => boolean = Impl.isSubInterval as any;
 
-    // export const areEqual: (a: Interval, b: Interval) => boolean = Impl.areEqual as any;
-    // export const areIntersecting: (a: Interval, b: Interval) => boolean = Impl.areIntersecting as any;
-    // export const isSubset: (a: Interval, b: Interval) => boolean = Impl.isSubset as any;
+    export const findPredecessorIndex: (interval: Interval, x: number) => number = Impl.findPredecessorIndex as any;
+    export const findPredecessorIndexInInterval: (interval: Interval, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
+    export const findRange: (interval: Interval, min: number, max: number) => Interval = Impl.findRange as any;
 
+    export const intersect: (a: Interval, b: Interval) => Interval = Impl.intersect as any;
 }
 
 interface Interval { '@type': 'int-interval' }
 
-export default Interval
-
-namespace Impl {export const Empty = IntTuple.Zero;
-
-    export function create(min: number, max: number) { return max < min ? Empty : IntTuple.create(min, max + 1); }
-
-    export const start = IntTuple.fst
-    export const end = IntTuple.snd
-    export const min = IntTuple.fst
-    export function max(i: IntTuple) { return IntTuple.snd(i) + 1; }
-    export function size(i: IntTuple) { return IntTuple.snd(i) - IntTuple.fst(i); }
-
-    export function has(int: IntTuple, v: number) { return IntTuple.fst(int) <= v && v < IntTuple.snd(int); }
-}
\ No newline at end of file
+export default Interval
\ No newline at end of file
diff --git a/src/mol-base/collections/sorted-array.ts b/src/mol-base/collections/sorted-array.ts
new file mode 100644
index 0000000000000000000000000000000000000000..11c5c31abb79d483fa9cbf204ab8133c71522585
--- /dev/null
+++ b/src/mol-base/collections/sorted-array.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as Impl from './impl/sorted-array'
+import Interval from './interval'
+
+namespace SortedArray {
+    /** Create interval [min, max] */
+    export const create: (xs: ArrayLike<number>) => SortedArray = Impl.ofUnsortedArray as any;
+    /** Create interval [min, max) */
+    export const ofSortedArray: (xs: ArrayLike<number>) => SortedArray = Impl.ofSortedArray as any;
+    export const is: (v: any) => v is Interval = Impl.is as any;
+
+    export const has: (interval: SortedArray, x: number) => boolean = Impl.has as any;
+    export const indexOf: (interval: SortedArray, x: number) => number = Impl.indexOf as any;
+    export const getAt: (interval: SortedArray, i: number) => number = Impl.getAt as any;
+
+    export const start: (interval: SortedArray) => number = Impl.start as any;
+    export const end: (interval: SortedArray) => number = Impl.end as any;
+    export const min: (interval: SortedArray) => number = Impl.min as any;
+    export const max: (interval: SortedArray) => number = Impl.max as any;
+    export const size: (interval: SortedArray) => number = Impl.size as any;
+    export const hashCode: (interval: SortedArray) => number = Impl.hashCode as any;
+
+    export const areEqual: (a: SortedArray, b: SortedArray) => boolean = Impl.areEqual as any;
+
+    export const findPredecessorIndex: (interval: SortedArray, x: number) => number = Impl.findPredecessorIndex as any;
+    export const findPredecessorIndexInInterval: (interval: SortedArray, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
+    export const findRange: (interval: SortedArray, min: number, max: number) => Interval = Impl.findRange as any;
+}
+
+interface SortedArray { '@type': 'int-sorted-array' }
+
+export default SortedArray
\ No newline at end of file