Index: lucene/analysis/common/src/test/org/apache/lucene/analysis/charfilter/TestMappingCharFilter.java
===================================================================
--- lucene/analysis/common/src/test/org/apache/lucene/analysis/charfilter/TestMappingCharFilter.java	(revision 1332271)
+++ lucene/analysis/common/src/test/org/apache/lucene/analysis/charfilter/TestMappingCharFilter.java	(working copy)
@@ -19,7 +19,11 @@
 
 import java.io.Reader;
 import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 
@@ -250,7 +254,7 @@
     //System.out.println("NormalizeCharMap=");
     for (int i = 0; i < num; i++) {
       String key = _TestUtil.randomSimpleString(random);
-      if (!keys.contains(key)) {
+      if (!keys.contains(key) && key.length() != 0) {
         String value = _TestUtil.randomSimpleString(random);
         map.add(key, value);
         keys.add(key);
@@ -259,4 +263,178 @@
     }
     return map;
   }
+
+  public void testRandomMaps2() throws Exception {
+    final Random random = random();
+    final int numIterations = atLeast(10);
+    for(int iter=0;iter<numIterations;iter++) {
+
+      if (VERBOSE) {
+        System.out.println("\nTEST iter=" + iter);
+      }
+
+      final char endLetter = (char) _TestUtil.nextInt(random, 'b', 'z');
+
+      final Map<String,String> map = new HashMap<String,String>();
+      final NormalizeCharMap charMap = new NormalizeCharMap();
+      final int numMappings = atLeast(5);
+      if (VERBOSE) {
+        System.out.println("  mappings:");
+      }
+      while (map.size() < numMappings) {
+        final String key = _TestUtil.randomSimpleStringRange(random, 'a', endLetter, 7);
+        if (key.length() != 0 && !map.containsKey(key)) {
+          final String value = _TestUtil.randomSimpleString(random);
+          map.put(key, value);
+          charMap.add(key, value);
+          if (VERBOSE) {
+            System.out.println("    " + key + " -> " + value);
+          }
+        }
+      }
+
+      if (VERBOSE) {
+        System.out.println("  test random documents...");
+      }
+
+      for(int iter2=0;iter2<100;iter2++) {
+        final String content = _TestUtil.randomSimpleStringRange(random, 'a', endLetter, atLeast(1000));
+
+        if (VERBOSE) {
+          System.out.println("  content=" + content);
+        }
+
+        // Do stupid dog-slow mapping:
+
+        // Output string:
+        final StringBuilder output = new StringBuilder();
+
+        // Maps output offset to input offset:
+        final List<Integer> inputOffsets = new ArrayList<Integer>();
+
+        int cumDiff = 0;
+        int charIdx = 0;
+        while(charIdx < content.length()) {
+
+          int matchLen = -1;
+          String matchRepl = null;
+
+          for(Map.Entry<String,String> ent : map.entrySet()) {
+            final String match = ent.getKey();
+            if (charIdx + match.length() <= content.length()) {
+              final int limit = charIdx+match.length();
+              boolean matches = true;
+              for(int charIdx2=charIdx;charIdx2<limit;charIdx2++) {
+                if (match.charAt(charIdx2-charIdx) != content.charAt(charIdx2)) {
+                  matches = false;
+                  break;
+                }
+              }
+
+              if (matches) {
+                final String repl = ent.getValue();
+                if (match.length() > matchLen) {
+                  // Greedy: longer match wins
+                  matchLen = match.length();
+                  matchRepl = repl;
+                }
+              }
+            }
+          }
+
+          if (matchLen != -1) {
+            // We found a match here!
+            if (VERBOSE) {
+              System.out.println("    match=" + content.substring(charIdx, charIdx+matchLen) + " @ off=" + charIdx + " repl=" + matchRepl);
+            }
+            output.append(matchRepl);
+            final int minLen = Math.min(matchLen, matchRepl.length());
+
+            // Common part, directly maps back to input
+            // offset:
+            for(int outIdx=0;outIdx<minLen;outIdx++) {
+              inputOffsets.add(output.length() - matchRepl.length() + outIdx + cumDiff);
+            }
+
+            cumDiff += matchLen - matchRepl.length();
+            charIdx += matchLen;
+
+            if (matchRepl.length() < matchLen) {
+              // Replacement string is shorter than matched
+              // input: nothing to do
+            } else if (matchRepl.length() > matchLen) {
+              // Replacement string is longer than matched
+              // input: for all the "extra" chars we map
+              // back to a single input offset:
+              for(int outIdx=matchLen;outIdx<matchRepl.length();outIdx++) {
+                inputOffsets.add(output.length() + cumDiff - 1);
+              }
+            } else {
+              // Same length: no change to offset
+            }
+
+            assert inputOffsets.size() == output.length(): "inputOffsets.size()=" + inputOffsets.size() + " vs output.length()=" + output.length();
+          } else {
+            inputOffsets.add(output.length() + cumDiff);
+            output.append(content.charAt(charIdx));
+            charIdx++;
+          }
+        }
+
+        final String expected = output.toString();
+        if (VERBOSE) {
+          System.out.print("    expected:");
+          for(int charIdx2=0;charIdx2<expected.length();charIdx2++) {
+            System.out.print(" " + ((char) expected.charAt(charIdx2) + "/" + inputOffsets.get(charIdx2)));
+          }
+          System.out.println();
+        }
+
+        final MappingCharFilter mapFilter = new MappingCharFilter(charMap, new StringReader(content));
+
+        final StringBuilder actualBuilder = new StringBuilder();
+        final List<Integer> actualInputOffsets = new ArrayList<Integer>();
+
+        // Now consume the actual mapFilter, somewhat randomly:
+        while (true) {
+          if (random.nextBoolean()) {
+            final int ch = mapFilter.read();
+            if (ch == -1) {
+              break;
+            }
+            actualBuilder.append((char) ch);
+          } else {
+            final char[] buffer = new char[_TestUtil.nextInt(random, 1, 100)];
+            final int off = buffer.length == 1 ? 0 : random.nextInt(buffer.length-1);
+            final int count = mapFilter.read(buffer, off, buffer.length-off);
+            if (count == -1) {
+              break;
+            } else {
+              actualBuilder.append(buffer, off, count);
+            }
+          }
+
+          if (random.nextInt(10) == 7) {
+            // Map offsets
+            while(actualInputOffsets.size() < actualBuilder.length()) {
+              actualInputOffsets.add(mapFilter.correctOffset(actualInputOffsets.size()));
+            }
+          }
+        }
+
+        // Finish mappping offsets
+        while(actualInputOffsets.size() < actualBuilder.length()) {
+          actualInputOffsets.add(mapFilter.correctOffset(actualInputOffsets.size()));
+        }
+
+        final String actual = actualBuilder.toString();
+
+        // Verify:
+        assertEquals(expected, actual);
+        assertEquals(inputOffsets, actualInputOffsets);
+      }        
+    }
+  }
+
+  // nocommit perf test
 }
Index: lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestRandomChains.java
===================================================================
--- lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestRandomChains.java	(revision 1332271)
+++ lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestRandomChains.java	(working copy)
@@ -56,7 +56,6 @@
 import org.apache.lucene.analysis.Tokenizer;
 import org.apache.lucene.analysis.wikipedia.WikipediaTokenizer;
 import org.apache.lucene.analysis.ValidatingTokenFilter;
-import org.apache.lucene.analysis.charfilter.CharFilter;
 import org.apache.lucene.analysis.charfilter.NormalizeCharMap;
 import org.apache.lucene.analysis.cjk.CJKBigramFilter;
 import org.apache.lucene.analysis.commongrams.CommonGramsFilter;
@@ -441,7 +440,7 @@
         //System.out.println("NormalizeCharMap=");
         for (int i = 0; i < num; i++) {
           String key = _TestUtil.randomSimpleString(random);
-          if (!keys.contains(key)) {
+          if (!keys.contains(key) && key.length() > 0) {
             String value = _TestUtil.randomSimpleString(random);
             map.add(key, value);
             keys.add(key);
Index: lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestBugInSomething.java
===================================================================
--- lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestBugInSomething.java	(revision 1332271)
+++ lucene/analysis/common/src/test/org/apache/lucene/analysis/core/TestBugInSomething.java	(working copy)
@@ -45,7 +45,6 @@
     map.add("mtqlpi", "");
     map.add("mwoknt", "jjp");
     map.add("tcgyreo", "zpfpajyws");
-    map.add("", "eethksv");
     
     Analyzer a = new Analyzer() {
       @Override
Index: lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/MappingCharFilter.java
===================================================================
--- lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/MappingCharFilter.java	(revision 1332271)
+++ lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/MappingCharFilter.java	(working copy)
@@ -19,126 +19,163 @@
 
 import java.io.IOException;
 import java.io.Reader;
-import java.util.LinkedList;
 
 import org.apache.lucene.analysis.CharReader;
 import org.apache.lucene.analysis.CharStream;
+import org.apache.lucene.util.CharsRef;
+import org.apache.lucene.util.RollingCharBuffer;
+import org.apache.lucene.util.fst.CharSequenceOutputs;
+import org.apache.lucene.util.fst.FST;
+import org.apache.lucene.util.fst.Outputs;
 
 /**
  * Simplistic {@link CharFilter} that applies the mappings
  * contained in a {@link NormalizeCharMap} to the character
  * stream, and correcting the resulting changes to the
- * offsets.
+ * offsets.  Matching is greedy (longest pattern matching at
+ * a given point wins).  Replacement is allowed to be the
+ * empty string.
  */
+
 public class MappingCharFilter extends BaseCharFilter {
 
-  private final NormalizeCharMap normMap;
-  private LinkedList<Character> buffer;
-  private String replacement;
-  private int charPointer;
-  private int nextCharCounter;
+  private final Outputs<CharsRef> outputs = CharSequenceOutputs.getSingleton();
+  private final FST<CharsRef> map;
+  private final FST.BytesReader fstReader;
+  private final RollingCharBuffer buffer = new RollingCharBuffer();
 
+  private CharsRef replacement;
+  private int replacementPointer;
+  private int inputOff;
+
   /** Default constructor that takes a {@link CharStream}. */
   public MappingCharFilter(NormalizeCharMap normMap, CharStream in) {
     super(in);
-    this.normMap = normMap;
+    map = normMap.getMap();
+    buffer.reset(in);
+    if (map != null) {
+      fstReader = map.getBytesReader(0);
+    } else {
+      fstReader = null;
+    }
   }
 
   /** Easy-use constructor that takes a {@link Reader}. */
   public MappingCharFilter(NormalizeCharMap normMap, Reader in) {
-    super(CharReader.get(in));
-    this.normMap = normMap;
+    this(normMap, CharReader.get(in));
   }
 
   @Override
+  public void reset() throws IOException {
+    super.reset();
+    buffer.reset(input);
+    replacement = null;
+    inputOff = 0;
+  }
+
+  private final FST.Arc<CharsRef> scratchArc = new FST.Arc<CharsRef>();
+
+  @Override
   public int read() throws IOException {
+    //System.out.println("\nread");
     while(true) {
-      if (replacement != null && charPointer < replacement.length()) {
-        return replacement.charAt(charPointer++);
+
+      if (replacement != null && replacementPointer < replacement.length) {
+        //System.out.println("  return repl[" + replacementPointer + "]=" + replacement.chars[replacement.offset + replacementPointer]);
+        return replacement.chars[replacement.offset + replacementPointer++];
       }
 
-      int firstChar = nextChar();
-      if (firstChar == -1) return -1;
-      NormalizeCharMap nm = normMap.submap != null ?
-        normMap.submap.get(Character.valueOf((char) firstChar)) : null;
-      if (nm == null) return firstChar;
-      NormalizeCharMap result = match(nm);
-      if (result == null) return firstChar;
-      replacement = result.normStr;
-      charPointer = 0;
-      if (result.diff != 0) {
-        int prevCumulativeDiff = getLastCumulativeDiff();
-        if (result.diff < 0) {
-          for(int i = 0; i < -result.diff ; i++)
-            addOffCorrectMap(nextCharCounter + i - prevCumulativeDiff, prevCumulativeDiff - 1 - i);
-        } else {
-          addOffCorrectMap(nextCharCounter - result.diff - prevCumulativeDiff, prevCumulativeDiff + result.diff);
+      // TODO: a more efficient approach would be Aho/Corasick's
+      // algorithm: http://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm
+      // I think this would be (almost?) equivalent to 1) adding
+      // epsilon arcs from all final nodes back to the init
+      // node in the FST, 2) adding a .* (skip any char)
+      // loop on the initial node, and 3) determinizing
+      // that.  Then we would not have to restart matching
+      // at each position.
+
+      CharsRef output = outputs.getNoOutput();
+      int lookahead = 0;
+      int lastMatchLen = -1;
+      CharsRef lastMatch = null;
+
+      // null map means no matches were added
+      if (map != null) {
+
+        map.getFirstArc(scratchArc);      
+        while (true) {
+          int ch = buffer.get(inputOff + lookahead);
+          if (ch == -1) {
+            break;
+          }
+          //System.out.println("  inputOff=" + (inputOff + lookahead) + ": got ch=" + (char) ch);
+
+          if (map.findTargetArc(ch, scratchArc, scratchArc, fstReader) == null) {
+            // Dead end
+            break;
+          }
+
+          lookahead++;
+          output = outputs.add(output, scratchArc.output);
+
+          if (scratchArc.isFinal()) {
+            // Match! (to node is final)
+            lastMatchLen = lookahead;
+            lastMatch = outputs.add(output, scratchArc.nextFinalOutput);
+            // Greedy: keep searching to see if there's a
+            // longer match...
+          }
         }
       }
-    }
-  }
 
-  private int nextChar() throws IOException {
-    if (buffer != null && !buffer.isEmpty()) {
-      nextCharCounter++;
-      return buffer.removeFirst().charValue();
-    }
-    int nextChar = input.read();
-    if (nextChar != -1) {
-      nextCharCounter++;
-    }
-    return nextChar;
-  }
+      if (lastMatch != null) {
+        replacement = lastMatch;
+        inputOff += lastMatchLen;
+        //System.out.println("  match!  len=" + lastMatchLen + " repl=" + lastMatch);
+        replacementPointer = 0;
 
-  private void pushChar(int c) {
-    nextCharCounter--;
-    if(buffer == null)
-      buffer = new LinkedList<Character>();
-    buffer.addFirst(Character.valueOf((char) c));
-  }
+        final int diff = lastMatchLen - replacement.length;
 
-  private void pushLastChar(int c) {
-    if (buffer == null) {
-      buffer = new LinkedList<Character>();
-    }
-    buffer.addLast(Character.valueOf((char) c));
-  }
+        final int prevCumulativeDiff = getLastCumulativeDiff();
 
-  private NormalizeCharMap match(NormalizeCharMap map) throws IOException {
-    NormalizeCharMap result = null;
-    if (map.submap != null) {
-      int chr = nextChar();
-      if (chr != -1) {
-        NormalizeCharMap subMap = map.submap.get(Character.valueOf((char) chr));
-        if (subMap != null) {
-          result = match(subMap);
+        if (diff > 0) {
+          // Replacement is shorter than matched input:
+          //System.out.println("    new cum diff " + (prevCumulativeDiff - diff));
+          addOffCorrectMap(inputOff - diff - prevCumulativeDiff, prevCumulativeDiff + diff);
+        } else if (diff < 0) {
+          // Replacement is longer than matched input: remap
+          // the "extra" chars all back to the same input
+          // offset:
+          final int outputStart = inputOff - prevCumulativeDiff;
+          for(int extraIDX=0;extraIDX<-diff;extraIDX++) {
+            addOffCorrectMap(outputStart + extraIDX, prevCumulativeDiff - extraIDX - 1);
+          }
+        } else {
+          // No change to offsets
         }
-        if (result == null) {
-          pushChar(chr);
+      } else {
+        final int ret = buffer.get(inputOff);
+        if (ret != -1) {
+          //System.out.println("  return pass through " + (char) ret);
+          inputOff++;
+        } else {
+          //System.out.println("  return pass through <END>");
         }
+        return ret;
       }
     }
-    if (result == null && map.normStr != null) {
-      result = map;
-    }
-    return result;
   }
 
   @Override
   public int read(char[] cbuf, int off, int len) throws IOException {
-    char[] tmp = new char[len];
-    int l = input.read(tmp, 0, len);
-    if (l != -1) {
-      for(int i = 0; i < l; i++)
-        pushLastChar(tmp[i]);
-    }
-    l = 0;
+    int numRead = 0;
     for(int i = off; i < off + len; i++) {
       int c = read();
       if (c == -1) break;
       cbuf[i] = (char) c;
-      l++;
+      numRead++;
     }
-    return l == 0 ? -1 : l;
+
+    return numRead == 0 ? -1 : numRead;
   }
 }
Index: lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/NormalizeCharMap.java
===================================================================
--- lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/NormalizeCharMap.java	(revision 1332271)
+++ lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/NormalizeCharMap.java	(working copy)
@@ -17,45 +17,69 @@
 
 package org.apache.lucene.analysis.charfilter;
 
-import java.util.HashMap;
+import java.io.IOException;
+import java.util.TreeMap;
 import java.util.Map;
 
+import org.apache.lucene.util.CharsRef;
+import org.apache.lucene.util.IntsRef;
+import org.apache.lucene.util.fst.Builder;
+import org.apache.lucene.util.fst.CharSequenceOutputs;
+import org.apache.lucene.util.fst.FST;
+import org.apache.lucene.util.fst.Outputs;
+import org.apache.lucene.util.fst.Util;
+
 /**
  * Holds a map of String input to String output, to be used
  * with {@link MappingCharFilter}.
  */
 public class NormalizeCharMap {
 
-  Map<Character, NormalizeCharMap> submap;
-  String normStr;
-  int diff;
+  private final Map<String,String> pendingPairs = new TreeMap<String,String>();
 
-  /** Records a replacement to be applied to the inputs
+  // Non-null once built
+  private FST<CharsRef> map;
+
+  /** Records a replacement to be applied to the input
    *  stream.  Whenever <code>singleMatch</code> occurs in
    *  the input, it will be replaced with
    *  <code>replacement</code>.
    *
-   * @param singleMatch input String to be replaced
+   * @param match input String to be replaced
    * @param replacement output String
    */
-  public void add(String singleMatch, String replacement) {
-    NormalizeCharMap currMap = this;
-    for(int i = 0; i < singleMatch.length(); i++) {
-      char c = singleMatch.charAt(i);
-      if (currMap.submap == null) {
-        currMap.submap = new HashMap<Character, NormalizeCharMap>(1);
-      }
-      NormalizeCharMap map = currMap.submap.get(Character.valueOf(c));
-      if (map == null) {
-        map = new NormalizeCharMap();
-        currMap.submap.put(Character.valueOf(c), map);
-      }
-      currMap = map;
+  public void add(String match, String replacement) {
+    if (map != null) {
+      throw new IllegalStateException("cannot add new matches once this instance has been used in MappingCharFilter");
     }
-    if (currMap.normStr != null) {
-      throw new RuntimeException("MappingCharFilter: there is already a mapping for " + singleMatch);
+    if (match.length() == 0 ){
+      throw new IllegalArgumentException("cannot match the empty string");
     }
-    currMap.normStr = replacement;
-    currMap.diff = singleMatch.length() - replacement.length();
+    if (pendingPairs.containsKey(match)) {
+      throw new IllegalArgumentException("match \"" + match + "\" was already added");
+    }
+    pendingPairs.put(match, replacement);
   }
+
+  FST<CharsRef> getMap() {
+    if (map == null) {
+      try {
+        final Outputs<CharsRef> outputs = CharSequenceOutputs.getSingleton();
+        final Builder<CharsRef> builder = new Builder<CharsRef>(FST.INPUT_TYPE.BYTE2, outputs);
+        final IntsRef scratch = new IntsRef();
+        for(Map.Entry<String,String> ent : pendingPairs.entrySet()) {
+          builder.add(Util.toUTF32(ent.getKey(), scratch),
+                      new CharsRef(ent.getValue()));
+      
+        }
+        map = builder.finish();
+        pendingPairs.clear();
+      } catch (IOException ioe) {
+        // Bogus FST IOExceptions!!  (will never happen)
+        throw new RuntimeException(ioe);
+      }
+    }
+
+    return map;
+  }
 }
Index: lucene/test-framework/src/java/org/apache/lucene/util/_TestUtil.java
===================================================================
--- lucene/test-framework/src/java/org/apache/lucene/util/_TestUtil.java	(revision 1332271)
+++ lucene/test-framework/src/java/org/apache/lucene/util/_TestUtil.java	(working copy)
@@ -206,6 +206,19 @@
     return new String(buffer, 0, end);
   }
 
+  public static String randomSimpleStringRange(Random r, char minChar, char maxChar, int maxLength) {
+    final int end = nextInt(r, 0, maxLength);
+    if (end == 0) {
+      // allow 0 length
+      return "";
+    }
+    final char[] buffer = new char[end];
+    for (int i = 0; i < end; i++) {
+      buffer[i] = (char) _TestUtil.nextInt(r, minChar, maxChar);
+    }
+    return new String(buffer, 0, end);
+  }
+
   public static String randomSimpleString(Random r) {
     return randomSimpleString(r, 10);
   }
Index: lucene/core/src/java/org/apache/lucene/util/fst/CharSequenceOutputs.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/fst/CharSequenceOutputs.java	(revision 0)
+++ lucene/core/src/java/org/apache/lucene/util/fst/CharSequenceOutputs.java	(working copy)
@@ -0,0 +1,145 @@
+package org.apache.lucene.util.fst;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.util.CharsRef;
+
+/**
+ * An FST {@link Outputs} implementation where each output
+ * is a sequence of bytes.
+ *
+ * @lucene.experimental
+ */
+
+public final class CharSequenceOutputs extends Outputs<CharsRef> {
+
+  private final static CharsRef NO_OUTPUT = new CharsRef();
+  private final static CharSequenceOutputs singleton = new CharSequenceOutputs();
+
+  private CharSequenceOutputs() {
+  }
+
+  public static CharSequenceOutputs getSingleton() {
+    return singleton;
+  }
+
+  @Override
+  public CharsRef common(CharsRef output1, CharsRef output2) {
+    assert output1 != null;
+    assert output2 != null;
+
+    int pos1 = output1.offset;
+    int pos2 = output2.offset;
+    int stopAt1 = pos1 + Math.min(output1.length, output2.length);
+    while(pos1 < stopAt1) {
+      if (output1.chars[pos1] != output2.chars[pos2]) {
+        break;
+      }
+      pos1++;
+      pos2++;
+    }
+
+    if (pos1 == output1.offset) {
+      // no common prefix
+      return NO_OUTPUT;
+    } else if (pos1 == output1.offset + output1.length) {
+      // output1 is a prefix of output2
+      return output1;
+    } else if (pos2 == output2.offset + output2.length) {
+      // output2 is a prefix of output1
+      return output2;
+    } else {
+      return new CharsRef(output1.chars, output1.offset, pos1-output1.offset);
+    }
+  }
+
+  @Override
+  public CharsRef subtract(CharsRef output, CharsRef inc) {
+    assert output != null;
+    assert inc != null;
+    if (inc == NO_OUTPUT) {
+      // no prefix removed
+      return output;
+    } else if (inc.length == output.length) {
+      // entire output removed
+      return NO_OUTPUT;
+    } else {
+      assert inc.length < output.length: "inc.length=" + inc.length + " vs output.length=" + output.length;
+      assert inc.length > 0;
+      return new CharsRef(output.chars, output.offset + inc.length, output.length-inc.length);
+    }
+  }
+
+  @Override
+  public CharsRef add(CharsRef prefix, CharsRef output) {
+    assert prefix != null;
+    assert output != null;
+    if (prefix == NO_OUTPUT) {
+      return output;
+    } else if (output == NO_OUTPUT) {
+      return prefix;
+    } else {
+      assert prefix.length > 0;
+      assert output.length > 0;
+      CharsRef result = new CharsRef(prefix.length + output.length);
+      System.arraycopy(prefix.chars, prefix.offset, result.chars, 0, prefix.length);
+      System.arraycopy(output.chars, output.offset, result.chars, prefix.length, output.length);
+      result.length = prefix.length + output.length;
+      return result;
+    }
+  }
+
+  @Override
+  public void write(CharsRef prefix, DataOutput out) throws IOException {
+    assert prefix != null;
+    out.writeVInt(prefix.length);
+    // nocommit utf8...?
+    for(int idx=0;idx<prefix.length;idx++) {
+      out.writeVInt(prefix.chars[prefix.offset+idx]);
+    }
+  }
+
+  @Override
+  public CharsRef read(DataInput in) throws IOException {
+    final int len = in.readVInt();
+    if (len == 0) {
+      return NO_OUTPUT;
+    } else {
+      final CharsRef output = new CharsRef(len);
+      for(int idx=0;idx<len;idx++) {
+        output.chars[idx] = (char) in.readVInt();
+      }
+      output.length = len;
+      return output;
+    }
+  }
+
+  @Override
+  public CharsRef getNoOutput() {
+    return NO_OUTPUT;
+  }
+
+  @Override
+  public String outputToString(CharsRef output) {
+    return output.toString();
+  }
+}

Property changes on: lucene/core/src/java/org/apache/lucene/util/fst/CharSequenceOutputs.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
Index: lucene/core/src/java/org/apache/lucene/util/fst/Builder.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/fst/Builder.java	(revision 1332271)
+++ lucene/core/src/java/org/apache/lucene/util/fst/Builder.java	(working copy)
@@ -304,7 +304,9 @@
 
   /** It's OK to add the same input twice in a row with
    *  different outputs, as long as outputs impls the merge
-   *  method. */
+   *  method.  NOTE: confusingly, input is fully consumed
+   *  (so caller can reuse) but output is not (so caller
+   *  cannot reuse). */
   public void add(IntsRef input, T output) throws IOException {
     /*
     if (DEBUG) {
Index: lucene/core/src/java/org/apache/lucene/util/CharsRef.java
===================================================================
--- lucene/core/src/java/org/apache/lucene/util/CharsRef.java	(revision 1332271)
+++ lucene/core/src/java/org/apache/lucene/util/CharsRef.java	(working copy)
@@ -1,7 +1,5 @@
 package org.apache.lucene.util;
 
-import java.util.Comparator;
-
 /**
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements.  See the NOTICE file distributed with
@@ -19,6 +17,8 @@
  * limitations under the License.
  */
 
+import java.util.Comparator;
+
 /**
  * Represents char[], as a slice (offset + length) into an existing char[].
  * The {@link #chars} member should never be null; use
