diff --git a/bits/40_harb.js b/bits/40_harb.js
index 52e8862..8e55348 100644
--- a/bits/40_harb.js
+++ b/bits/40_harb.js
@@ -409,7 +409,10 @@ var SYLK = /*#__PURE__*/(function() {
 		"!":161, '"':162, "#":163, "(":164, "%":165, "'":167, "H ":168,
 		"+":171, ";":187, "<":188, "=":189, ">":190, "?":191, "{":223
 	}/*:any*/);
-	var sylk_char_regex = new RegExp("\u001BN(" + keys(sylk_escapes).join("|").replace(/\|\|\|/, "|\\||").replace(/([?()+])/g,"\\$1") + "|\\|)", "gm");
+	var sylk_char_regex = new RegExp("\u001BN(" + keys(sylk_escapes).join("|").replace(/\|\|\|/, "|\\||").replace(/([?()+])/g,"\\$1").replace("{", "\\{") + "|\\|)", "gm");
+	try {
+		sylk_char_regex = new RegExp("\u001BN(" + keys(sylk_escapes).join("|").replace(/\|\|\|/, "|\\||").replace(/([?()+])/g,"\\$1") + "|\\|)", "gm");
+	} catch(e) {}
 	var sylk_char_fn = function(_, $1){ var o = sylk_escapes[$1]; return typeof o == "number" ? _getansi(o) : o; };
 	var decode_sylk_char = function($$, $1, $2) { var newcc = (($1.charCodeAt(0) - 0x20)<<4) | ($2.charCodeAt(0) - 0x30); return newcc == 59 ? $$ : _getansi(newcc); };
 	sylk_escapes["|"] = 254;
diff --git a/bits/41_lotus.js b/bits/41_lotus.js
index 0996676..f39c464 100644
--- a/bits/41_lotus.js
+++ b/bits/41_lotus.js
@@ -977,13 +977,25 @@ var WK_ = /*#__PURE__*/(function() {
 		/*::[*/0x0E/*::*/: "dd-mmm-yyyy",
 		/*::[*/0x0F/*::*/: "mmm-yyyy",
 
-		/*::[*/0x22/*::*/: "0.00",
-		/*::[*/0x32/*::*/: "0.00;[Red]0.00",
-		/*::[*/0x42/*::*/: "0.00;\(0.00\)",
-		/*::[*/0x52/*::*/: "0.00;[Red]\(0.00\)",
-
-		/*::[*/162/*::*/: '"$"#,##0;\\("$"#,##0\\)' // slightly different from SSF 5
+		/* It is suspected that the the low nybble specifies decimal places
+		/*::[*/0x0022/*::*/: "0.00",
+		/*::[*/0x0032/*::*/: "0.00;[Red]0.00",
+		/*::[*/0x0042/*::*/: "0.00;\(0.00\)",
+		/*::[*/0x0052/*::*/: "0.00;[Red]\(0.00\)",
+		/*::[*/0x00A2/*::*/: '"$"#,##0.00;\\("$"#,##0.00\\)',
+		/*::[*/0x0120/*::*/: '0%',
+		/*::[*/0x0130/*::*/: '0E+00',
+		/*::[*/0x0140/*::*/: '# ?/?'
 	};
+
+	function parse_qpw_str(p) {
+		var cch = p.read_shift(2);
+		var flags = p.read_shift(1);
+		/* TODO: find examples with nonzero flags */
+		if(flags != 0) throw "unsupported QPW string type " + flags.toString(16);
+		return p.read_shift(cch, "sbcs-cont");
+	}
+
 	/* QPW uses a different set of record types */
 	function qpw_to_workbook_buf(d, opts)/*:Workbook*/ {
 		prep_blob(d, 0);
@@ -1094,7 +1106,11 @@ var WK_ = /*#__PURE__*/(function() {
 							case 4: cell = { t: "n", v: parse_RkNumber(p) }; break;
 							case 5: cell = { t: "n", v: p.read_shift(8, 'f') }; break;
 							case 7: cell = { t: "s", v: SST[type = p.read_shift(4) - 1] }; break;
-							case 8: cell = { t: "n", v: p.read_shift(8, 'f') }; p.l += 2; /* cell.f = formulae[p.read_shift(4)]; */ p.l += 4; break;
+							case 8:
+								cell = { t: "n", v: p.read_shift(8, 'f') };
+								p.l += 2; /* cell.f = formulae[p.read_shift(4)]; */ p.l += 4;
+								if(isNaN(cell.v)) cell = { t: "e", v: 0x0F }; // #VALUE!
+								break;
 							default: throw "Unrecognized QPW cell type " + (flags & 0x1F);
 						}
 						if(fmtidx != -1 && (FMTS[fmtidx - 1]||{}).z) cell.z = FMTS[fmtidx-1].z;
@@ -1140,6 +1156,17 @@ var WK_ = /*#__PURE__*/(function() {
 					}
 				} break;
 
+				case 0x0C02: { /* String (result of string formula expression) */
+					C = p.read_shift(2);
+					R = p.read_shift(4);
+					var str = parse_qpw_str(p);
+					/* TODO: QP10 record has an additional unknown character after the string */
+					if(s["!data"] != null) {
+						if(!s["!data"][R]) s["!data"][R] = [];
+						s["!data"][R][C] = { t:"s", v:str };
+					} else s[encode_col(C) + encode_row(R)] = { t:"s", v:str };
+				} break;
+
 				default: break;
 			}
 			d.l += length;
diff --git a/bits/61_fcommon.js b/bits/61_fcommon.js
index 3171974..165b096 100644
--- a/bits/61_fcommon.js
+++ b/bits/61_fcommon.js
@@ -23,7 +23,10 @@ var rc_to_a1 = /*#__PURE__*/(function(){
 	};
 })();
 
-var crefregex = /(^|[^._A-Z0-9])([$]?)([A-Z]{1,2}|[A-W][A-Z]{2}|X[A-E][A-Z]|XF[A-D])([$]?)(10[0-3]\d{4}|104[0-7]\d{3}|1048[0-4]\d{2}|10485[0-6]\d|104857[0-6]|[1-9]\d{0,5})(?![_.\(A-Za-z0-9])/g;
+var crefregex = /(^|[^._A-Z0-9])(\$?)([A-Z]{1,2}|[A-W][A-Z]{2}|X[A-E][A-Z]|XF[A-D])(\$?)(\d{1,7})(?![_.\(A-Za-z0-9])/g;
+try {
+	crefregex = /(^|[^._A-Z0-9])([$]?)([A-Z]{1,2}|[A-W][A-Z]{2}|X[A-E][A-Z]|XF[A-D])([$]?)(10[0-3]\d{4}|104[0-7]\d{3}|1048[0-4]\d{2}|10485[0-6]\d|104857[0-6]|[1-9]\d{0,5})(?![_.\(A-Za-z0-9])/g;
+}catch(e){}
 var a1_to_rc = /*#__PURE__*/(function(){
 	return function a1_to_rc(fstr/*:string*/, base/*:CellAddress*/) {
 		return fstr.replace(crefregex, function($0, $1, $2, $3, $4, $5) {
diff --git a/bits/83_numbers.js b/bits/83_numbers.js
index d281a9e..7d02754 100644
--- a/bits/83_numbers.js
+++ b/bits/83_numbers.js
@@ -1117,7 +1117,7 @@ function s5s_to_iwa_comment(s5s) {
   return out;
 }
 function parse_TST_TableModelArchive(M, root, ws, opts) {
-  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
+  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
   var pb = parse_shallow(root.data);
   var range = { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } };
   range.e.r = (varint_to_i32(pb[6][0].data) >>> 0) - 1;
@@ -1146,7 +1146,17 @@ function parse_TST_TableModelArchive(M, root, ws, opts) {
     lut.nfmt = parse_TST_TableDataList(M, M[parse_TSP_Reference(store[22][0].data)][0]);
   var tile = parse_shallow(store[3][0].data);
   var _R = 0;
-  tile[1].forEach(function(t) {
+  if (!((_h = store[9]) == null ? void 0 : _h[0]))
+    throw "NUMBERS file missing row tree";
+  var rtt = parse_shallow(store[9][0].data)[1].map(function(p) {
+    return parse_shallow(p.data);
+  });
+  rtt.forEach(function(kv) {
+    _R = varint_to_i32(kv[1][0].data);
+    var tidx = varint_to_i32(kv[2][0].data);
+    var t = tile[1][tidx];
+    if (!t)
+      throw "NUMBERS missing tile " + tidx;
     var tl = parse_shallow(t.data);
     var ref2 = M[parse_TSP_Reference(tl[2][0].data)][0];
     var mtype2 = varint_to_i32(ref2.meta[1][0].data);
@@ -1169,12 +1179,12 @@ function parse_TST_TableModelArchive(M, root, ws, opts) {
     });
     _R += _tile.nrows;
   });
-  if ((_h = store[13]) == null ? void 0 : _h[0]) {
+  if ((_i = store[13]) == null ? void 0 : _i[0]) {
     var ref = M[parse_TSP_Reference(store[13][0].data)][0];
     var mtype = varint_to_i32(ref.meta[1][0].data);
     if (mtype != 6144)
       throw new Error("Expected merge type 6144, found ".concat(mtype));
-    ws["!merges"] = (_i = parse_shallow(ref.data)) == null ? void 0 : _i[1].map(function(pi) {
+    ws["!merges"] = (_j = parse_shallow(ref.data)) == null ? void 0 : _j[1].map(function(pi) {
       var merge = parse_shallow(pi.data);
       var origin = u8_to_dataview(parse_shallow(merge[1][0].data)[1][0].data), size = u8_to_dataview(parse_shallow(merge[2][0].data)[1][0].data);
       return {
diff --git a/modules/83_numbers.js b/modules/83_numbers.js
index d281a9e..7d02754 100644
--- a/modules/83_numbers.js
+++ b/modules/83_numbers.js
@@ -1117,7 +1117,7 @@ function s5s_to_iwa_comment(s5s) {
   return out;
 }
 function parse_TST_TableModelArchive(M, root, ws, opts) {
-  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
+  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
   var pb = parse_shallow(root.data);
   var range = { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } };
   range.e.r = (varint_to_i32(pb[6][0].data) >>> 0) - 1;
@@ -1146,7 +1146,17 @@ function parse_TST_TableModelArchive(M, root, ws, opts) {
     lut.nfmt = parse_TST_TableDataList(M, M[parse_TSP_Reference(store[22][0].data)][0]);
   var tile = parse_shallow(store[3][0].data);
   var _R = 0;
-  tile[1].forEach(function(t) {
+  if (!((_h = store[9]) == null ? void 0 : _h[0]))
+    throw "NUMBERS file missing row tree";
+  var rtt = parse_shallow(store[9][0].data)[1].map(function(p) {
+    return parse_shallow(p.data);
+  });
+  rtt.forEach(function(kv) {
+    _R = varint_to_i32(kv[1][0].data);
+    var tidx = varint_to_i32(kv[2][0].data);
+    var t = tile[1][tidx];
+    if (!t)
+      throw "NUMBERS missing tile " + tidx;
     var tl = parse_shallow(t.data);
     var ref2 = M[parse_TSP_Reference(tl[2][0].data)][0];
     var mtype2 = varint_to_i32(ref2.meta[1][0].data);
@@ -1169,12 +1179,12 @@ function parse_TST_TableModelArchive(M, root, ws, opts) {
     });
     _R += _tile.nrows;
   });
-  if ((_h = store[13]) == null ? void 0 : _h[0]) {
+  if ((_i = store[13]) == null ? void 0 : _i[0]) {
     var ref = M[parse_TSP_Reference(store[13][0].data)][0];
     var mtype = varint_to_i32(ref.meta[1][0].data);
     if (mtype != 6144)
       throw new Error("Expected merge type 6144, found ".concat(mtype));
-    ws["!merges"] = (_i = parse_shallow(ref.data)) == null ? void 0 : _i[1].map(function(pi) {
+    ws["!merges"] = (_j = parse_shallow(ref.data)) == null ? void 0 : _j[1].map(function(pi) {
       var merge = parse_shallow(pi.data);
       var origin = u8_to_dataview(parse_shallow(merge[1][0].data)[1][0].data), size = u8_to_dataview(parse_shallow(merge[2][0].data)[1][0].data);
       return {
diff --git a/modules/83_numbers.ts b/modules/83_numbers.ts
index d0271ac..fe1b18d 100644
--- a/modules/83_numbers.ts
+++ b/modules/83_numbers.ts
@@ -926,8 +926,18 @@ function parse_TST_TableModelArchive(M: MessageSpace, root: IWAMessage, ws: Work
 	// .TST.TileStorage
 	var tile = parse_shallow(store[3][0].data);
 	var _R = 0;
-	/* TODO: should this list be sorted by id ? */
-	tile[1].forEach(t => {
+
+	// .TST.TableRBTree
+	if(!store[9]?.[0]) throw "NUMBERS file missing row tree";
+	var rtt = parse_shallow(store[9][0].data)[1].map(p => parse_shallow(p.data));
+
+	/* TODO: check examples with ctt */
+	rtt.forEach(kv => {
+		// .TST.TableRBTree.Node
+		_R = varint_to_i32(kv[1][0].data);
+		var tidx = varint_to_i32(kv[2][0].data);
+		var t = tile[1][tidx];
+		if(!t) throw "NUMBERS missing tile " + tidx;
 		var tl = (parse_shallow(t.data));
 		// var id = varint_to_i32(tl[1][0].data);
 		var ref = M[parse_TSP_Reference(tl[2][0].data)][0];