diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/Builder.java b/lucene/core/src/java/org/apache/lucene/util/fst/Builder.java
index 428edd3..91e46c5 100644
--- a/lucene/core/src/java/org/apache/lucene/util/fst/Builder.java
+++ b/lucene/core/src/java/org/apache/lucene/util/fst/Builder.java
@@ -20,6 +20,7 @@ package org.apache.lucene.util.fst;
 import java.io.IOException;
 
 import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.IntsRef;
 import org.apache.lucene.util.IntsRefBuilder;
 import org.apache.lucene.util.fst.FST.INPUT_TYPE; // javadoc
@@ -210,7 +211,6 @@ public class Builder<T> {
       assert bytesPosEnd > bytesPosStart;
       lastFrozenNode = node;
     }
-
     nodeIn.clear();
 
     final CompiledNode fn = new CompiledNode();
@@ -344,9 +344,9 @@ public class Builder<T> {
       }
       b.length = input.length;
       if (output == NO_OUTPUT) {
-        System.out.println("\nFST ADD: input=" + toString(b) + " " + b);
+        System.out.println("\nFST ADD: input=" + b);
       } else {
-        System.out.println("\nFST ADD: input=" + toString(b) + " " + b + " output=" + fst.outputs.outputToString(output));
+        System.out.println("\nFST ADD: input=" + b + " output=" + fst.outputs.outputToString(output));
       }
     }
     */
@@ -480,7 +480,9 @@ public class Builder<T> {
       }
     }
     //if (DEBUG) System.out.println("  builder.finish root.isFinal=" + root.isFinal + " root.output=" + root.output);
-    fst.finish(compileNode(root, lastInput.length()).node);
+    long newStartNode = compileNode(root, lastInput.length()).node;
+    assert newStartNode == fst.bytes.getPosition() - 1 || newStartNode == -1;
+    fst.finish(newStartNode);
 
     return fst;
   }
diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/FST.java b/lucene/core/src/java/org/apache/lucene/util/fst/FST.java
index 8e84e3c..230c97b 100644
--- a/lucene/core/src/java/org/apache/lucene/util/fst/FST.java
+++ b/lucene/core/src/java/org/apache/lucene/util/fst/FST.java
@@ -24,6 +24,8 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
 
 import org.apache.lucene.codecs.CodecUtil;
 import org.apache.lucene.store.ByteArrayDataOutput;
@@ -116,6 +118,11 @@ public final class FST<T> implements Accountable {
   /** If arc has this label then that arc is final/accepted */
   public static final int END_LABEL = -1;
 
+  // FSTs are serialized in reverse order while building them, but un-reversed when written to disk,
+  // so finished FSTs, and those loaded from disk are in forward order (not reversed)
+  private boolean forward;
+  private long numBytes;        // used to reverse arc target addresses
+
   public final INPUT_TYPE inputType;
 
   // if non-null, this FST accepts the empty string and
@@ -245,6 +252,7 @@ public final class FST<T> implements Accountable {
     bytes.writeByte((byte) 0);
 
     emptyOutput = null;
+    forward = false;
   }
 
   public static final int DEFAULT_MAX_BLOCK_BITS = Constants.JRE_IS_64BIT ? 30 : 28;
@@ -258,6 +266,7 @@ public final class FST<T> implements Accountable {
    *  control the size of the byte[] pages used to hold the FST bytes. */
   public FST(DataInput in, Outputs<T> outputs, int maxBlockBits) throws IOException {
     this.outputs = outputs;
+    forward = true;
 
     if (maxBlockBits < 1 || maxBlockBits > 30) {
       throw new IllegalArgumentException("maxBlockBits should be 1 .. 30; got " + maxBlockBits);
@@ -274,13 +283,7 @@ public final class FST<T> implements Accountable {
       emptyBytes.copyBytes(in, numBytes);
 
       // De-serialize empty-string output:
-      BytesReader reader = emptyBytes.getReverseReader();
-      // NoOutputs uses 0 bytes when writing its output,
-      // so we have to check here else BytesStore gets
-      // angry:
-      if (numBytes > 0) {
-        reader.setPosition(numBytes-1);
-      }
+      BytesReader reader = emptyBytes.getForwardReader();
       emptyOutput = outputs.readFinalOutput(reader);
     } else {
       emptyOutput = null;
@@ -301,7 +304,7 @@ public final class FST<T> implements Accountable {
     }
     startNode = in.readVLong();
 
-    long numBytes = in.readVLong();
+    numBytes = in.readVLong();
     if (numBytes > 1 << maxBlockBits) {
       // FST is big: we need multiple pages
       bytes = new BytesStore(in, numBytes, 1<<maxBlockBits);
@@ -316,10 +319,6 @@ public final class FST<T> implements Accountable {
     cacheRootArcs();
   }
 
-  public INPUT_TYPE getInputType() {
-    return inputType;
-  }
-
   private long ramBytesUsed(Arc<T>[] arcs) {
     long size = 0;
     if (arcs != null) {
@@ -358,16 +357,28 @@ public final class FST<T> implements Accountable {
     return getClass().getSimpleName() + "(input=" + inputType + ",output=" + outputs;
   }
 
+  @Override
+  public Collection<Accountable> getChildResources() {
+    return Collections.emptySet();
+  }
+
   void finish(long newStartNode) throws IOException {
     assert newStartNode <= bytes.getPosition();
     if (startNode != -1) {
       throw new IllegalStateException("already finished");
     }
     if (newStartNode == FINAL_END_NODE && emptyOutput != null) {
-      newStartNode = 0;
+      startNode = 0;
+    } else {
+      startNode = bytes.getPosition() - newStartNode;
     }
-    startNode = newStartNode;
     bytes.finish();
+    // nocommit: expensive, but let's see if it helps us at query time
+    numBytes = bytes.getPosition();
+    if (numBytes > 2) {
+      bytes.reverse(1, numBytes - 1);
+    }
+    forward = true;
     cacheRootArcs();
   }
   
@@ -436,15 +447,6 @@ public final class FST<T> implements Accountable {
       outputs.writeFinalOutput(emptyOutput, ros);
       byte[] emptyOutputBytes = ros.toArrayCopy();
 
-      // reverse
-      final int stopAt = emptyOutputBytes.length/2;
-      int upto = 0;
-      while (upto < stopAt) {
-        final byte b = emptyOutputBytes[upto];
-        emptyOutputBytes[upto] = emptyOutputBytes[emptyOutputBytes.length-upto-1];
-        emptyOutputBytes[emptyOutputBytes.length-upto-1] = b;
-        upto++;
-      }
       out.writeVInt(emptyOutputBytes.length);
       out.writeBytes(emptyOutputBytes, 0, emptyOutputBytes.length);
     } else {
@@ -523,8 +525,8 @@ public final class FST<T> implements Accountable {
     return arc.target > 0;
   }
 
-  // serializes new node by appending its bytes to the end
-  // of the current byte[]
+  // serializes new node by appending its bytes to the end of the current byte[]
+  // for each arc: { byte -- flags, VInt -- label, output?, final-output?, target.node (unless implicitly next node) }
   long addNode(Builder<T> builder, Builder.UnCompiledNode<T> nodeIn) throws IOException {
     T NO_OUTPUT = outputs.getNoOutput();
 
@@ -683,11 +685,10 @@ public final class FST<T> implements Accountable {
       builder.bytes.writeBytes(startAddress, header, 0, headerLen);
     }
 
-    final long thisNodeAddress = builder.bytes.getPosition()-1;
+    builder.nodeCount++;
 
+    final long thisNodeAddress = builder.bytes.getPosition()-1;
     builder.bytes.reverse(startAddress, thisNodeAddress);
-
-    builder.nodeCount++;
     return thisNodeAddress;
   }
 
@@ -807,8 +808,7 @@ public final class FST<T> implements Accountable {
   public Arc<T> readFirstRealTargetArc(long node, Arc<T> arc, final BytesReader in) throws IOException {
     final long address = node;
     in.setPosition(address);
-    //System.out.println("  readFirstRealTargtArc address="
-    //+ address);
+    //System.out.println("  readFirstRealTargetArc address=" + address);
     //System.out.println("   flags=" + arc.flags);
 
     if (in.readByte() == ARCS_AS_FIXED_ARRAY) {
@@ -818,7 +818,7 @@ public final class FST<T> implements Accountable {
       arc.bytesPerArc = in.readVInt();
       arc.arcIdx = -1;
       arc.nextArc = arc.posArcsStart = in.getPosition();
-      //System.out.println("  bytesPer=" + arc.bytesPerArc + " numArcs=" + arc.numArcs + " arcsStart=" + pos);
+      // System.out.println("  bytesPer=" + arc.bytesPerArc + " numArcs=" + arc.numArcs + " arcsStart=" + arc.posArcsStart);
     } else {
       //arc.flags = b;
       arc.nextArc = address;
@@ -951,6 +951,9 @@ public final class FST<T> implements Accountable {
       arc.target = in.getPosition();
     } else {
       arc.target = readUnpackedNodeTarget(in);
+      if (forward && arc.target > 0) {
+        arc.target = numBytes - arc.target; // minimum value is 1
+      }
       arc.nextArc = in.getPosition();
     }
     return arc;
@@ -1138,9 +1141,9 @@ public final class FST<T> implements Accountable {
    *  position 0. */
   public BytesReader getBytesReader() {
     if (bytesArray != null) {
-      return new ReverseBytesReader(bytesArray);
+      return new ForwardBytesReader(bytesArray);
     } else {
-      return bytes.getReverseReader();
+      return bytes.getForwardReader();
     }
   }
 
diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java b/lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java
index 6704cf6..284d76c 100644
--- a/lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java
+++ b/lucene/core/src/java/org/apache/lucene/util/fst/NodeHash.java
@@ -23,6 +23,7 @@ import org.apache.lucene.util.packed.PackedInts;
 import org.apache.lucene.util.packed.PagedGrowableWriter;
 
 // Used to dedup states (lookup already-frozen states)
+// The hash of a node incorporates the arc's label, target, output, final-output, and is-final flag
 final class NodeHash<T> {
 
   private PagedGrowableWriter table;
@@ -110,7 +111,7 @@ final class NodeHash<T> {
       }
       fst.readNextRealArc(scratchArc, in);
     }
-    //System.out.println("  ret " + (h&Integer.MAX_VALUE));
+    // System.out.println("  ret " + (h&Integer.MAX_VALUE));
     return h & Long.MAX_VALUE;
   }
 
diff --git a/lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java b/lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java
index d882fae..3bc8c74 100644
--- a/lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java
+++ b/lucene/core/src/test/org/apache/lucene/util/fst/TestFSTs.java
@@ -53,6 +53,8 @@ import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.ByteArrayDataOutput;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.FSDirectory;
 import org.apache.lucene.store.IOContext;
@@ -263,7 +265,6 @@ public class TestFSTs extends LuceneTestCase {
 
   }
 
-
   public void testRandomWords() throws IOException {
     if (TEST_NIGHTLY) {
       testRandomWords(1000, atLeast(2));
@@ -303,7 +304,44 @@ public class TestFSTs extends LuceneTestCase {
   public void testBigSet() throws IOException {
     testRandomWords(TestUtil.nextInt(random(), 50000, 60000), 1);
   }
-  
+
+  // test some degenerate cases
+  public void testEmpty() throws IOException {
+    Builder<Long> builder = new Builder<Long>(FST.INPUT_TYPE.BYTE1, PositiveIntOutputs.getSingleton());
+    assertNull(builder.finish());
+    builder = new Builder<>(FST.INPUT_TYPE.BYTE1, PositiveIntOutputs.getSingleton());
+    builder.add(new IntsRef(), 7L);
+    FST<Long> fst = builder.finish();
+    assertNotNull(fst);
+    assertEquals(7L, (long) Util.get(fst, new IntsRef()));
+  }
+
+  // Simple test to make sure we save and restore properly
+  public void testReadWrite() throws IOException {
+    Builder<Long> builder = new Builder<>(FST.INPUT_TYPE.BYTE1, PositiveIntOutputs.getSingleton());
+    Set<IntsRef> words = new HashSet<>();
+    if (random().nextBoolean()) {
+        words.add(new IntsRef());   // include empty 
+    }
+    for (int i = 0; i < atLeast(100); i++) {
+      IntsRef word = toIntsRef(getRandomString(random()), 0);
+      words.add(word);
+    }
+    List<IntsRef> wordList = new ArrayList<>(words);
+    Collections.sort(wordList);
+    for (int i = 0; i < wordList.size(); i++) {
+      builder.add(wordList.get(i), (long) i);
+    }
+    FST<Long> fst = builder.finish();
+    byte[] buf = new byte[(int) fst.bytes.getPosition() + 100];
+    ByteArrayDataOutput bado = new ByteArrayDataOutput(buf);
+    fst.save(bado);
+    fst = new FST<>(new ByteArrayDataInput(buf), PositiveIntOutputs.getSingleton());
+    for (IntsRef word : words) {
+      assertNotNull(Util.get(fst, word));
+    }
+  }
+
   // Build FST for all unique terms in the test line docs
   // file, up until a doc limit
   public void testRealTerms() throws Exception {
@@ -988,7 +1026,7 @@ public class TestFSTs extends LuceneTestCase {
     }
 
     final Set<String> allTerms = new HashSet<>();
-    while(allTerms.size() < NUM_TERMS) {
+    while(allTerms.size() < 10000) {
       allTerms.add(simpleRandomString(random()));
     }
 
@@ -1001,6 +1039,7 @@ public class TestFSTs extends LuceneTestCase {
     if (VERBOSE) {
       System.out.println("TEST: get reader");
     }
+    w.commit();
     IndexReader r = w.getReader();
     if (VERBOSE) {
       System.out.println("TEST: got reader=" + r);
diff --git a/lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java b/lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java
index 8e6a4ea..7d2b417 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/util/fst/FSTTester.java
@@ -268,18 +268,25 @@ public class FSTTester<T> {
 
 
   FST<T> doTest(int prune1, int prune2, boolean allowRandomSuffixSharing) throws IOException {
+    boolean shareSuffixes = allowRandomSuffixSharing ? random.nextBoolean() : true;
+    int suffixSharingLimit = allowRandomSuffixSharing ? TestUtil.nextInt(random, 1, 10) : Integer.MAX_VALUE;
+
     if (LuceneTestCase.VERBOSE) {
-      System.out.println("\nTEST: prune1=" + prune1 + " prune2=" + prune2);
+      System.out.println("\nTEST: prune1=" + prune1 + " prune2=" + prune2 + " shareSuffixes=" + shareSuffixes + " suffixSharingLimit=" + suffixSharingLimit + ", inputMode=" + inputMode);
+      System.out.println("  expected input/output pairs:");
+      for(InputOutput<T> pair : pairs) {
+          System.out.println("  " + pair.input + " -> " + pair.output);
+      }
     }
 
     final Builder<T> builder = new Builder<>(inputMode == 0 ? FST.INPUT_TYPE.BYTE1 : FST.INPUT_TYPE.BYTE4,
-                                              prune1, prune2,
-                                              prune1==0 && prune2==0,
-                                              allowRandomSuffixSharing ? random.nextBoolean() : true,
-                                              allowRandomSuffixSharing ? TestUtil.nextInt(random, 1, 10) : Integer.MAX_VALUE,
-                                              outputs,
-                                              true,
-                                              15);
+                                             prune1, prune2,
+                                             prune1==0 && prune2==0,
+                                             shareSuffixes,
+                                             suffixSharingLimit,
+                                             outputs,
+                                             true,
+                                             15);
 
     for(InputOutput<T> pair : pairs) {
       if (pair.output instanceof List) {
@@ -388,18 +395,22 @@ public class FSTTester<T> {
     }
     {
       IntsRefFSTEnum<T> fstEnum = new IntsRefFSTEnum<>(fst);
+      int i = 0;
       for(InputOutput<T> pair : pairs) {
+        ++i;
         IntsRef term = pair.input;
         if (LuceneTestCase.VERBOSE) {
           System.out.println("TEST: check term=" + inputToString(inputMode, term) + " output=" + fst.outputs.outputToString(pair.output));
         }
         T output = run(fst, term, null);
         assertNotNull("term " + inputToString(inputMode, term) + " is not accepted", output);
-        assertTrue(outputsEqual(pair.output, output));
+        assertEquals("unexpected output for term " + term, pair.output, output);
 
         // verify enum's next
         IntsRefFSTEnum.InputOutput<T> t = fstEnum.next();
-        assertNotNull(t);
+        if (t == null) {
+          assertNotNull("got null for input [" + i + "] = " + inputToString(inputMode, term), t);
+        }
         assertEquals("expected input=" + inputToString(inputMode, term) + " but fstEnum returned " + inputToString(inputMode, t.input), term, t.input);
         assertTrue(outputsEqual(pair.output, t.output));
       }
@@ -783,9 +794,9 @@ public class FSTTester<T> {
       assertTrue(cmo.isLeaf || cmo.isFinal);
       //if (cmo.isFinal && !cmo.isLeaf) {
       if (cmo.isFinal) {
-        assertEquals(cmo.finalOutput, current.output);
+        assertEquals("unexpected final output for input " + current.input, cmo.finalOutput, current.output);
       } else {
-        assertEquals(cmo.output, current.output);
+        assertEquals("unexpected output for input " + current.input, cmo.output, current.output);
       }
     }
 
