Merge branch 'master' of github.com:googlei18n/libaddressinput into opensource_update_8565
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
index fea3035..1ace84f 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
@@ -64,12 +64,12 @@
     EnumSet<AddressField> visibleFields = EnumSet.noneOf(AddressField.class);
     List<AddressField> fieldOrder = new ArrayList<AddressField>();
     // TODO: Change this to just enumerate the address fields directly.
-    for (String substring : getFormatSubStrings(scriptType, regionCode)) {
+    for (String substring : getFormatSubstrings(scriptType, regionCode)) {
       // Skips un-escaped characters and new lines.
       if (!substring.matches("%.") || substring.equals(NEW_LINE)) {
         continue;
       }
-      AddressField field = AddressField.of(substring.charAt(1));
+      AddressField field = getFieldForFormatSubstring(substring);
       // Accept only the first instance for any duplicate fields (which can occur because the
       // string we start with defines format order, which can contain duplicate fields).
       if (!visibleFields.contains(field)) {
@@ -90,6 +90,34 @@
     return Collections.unmodifiableList(fieldOrder);
   }
 
+  /**
+   * Returns true if this format substring (e.g. %C) represents an address field. Returns false if
+   * it is a literal or newline.
+   */
+  private static boolean formatSubstringRepresentsField(String formatSubstring) {
+    return !formatSubstring.equals(NEW_LINE) && formatSubstring.startsWith("%");
+  }
+
+  /**
+   * Gets data from the address represented by a format substring such as %C. Will throw an
+   * exception if no field can be found.
+   */
+  private static AddressField getFieldForFormatSubstring(String formatSubstring) {
+    return AddressField.of(formatSubstring.charAt(1));
+  }
+
+  /**
+   * Returns true if the address has any data for this address field.
+   */
+  private static boolean addressHasValueForField(AddressData address, AddressField field) {
+    if (field == AddressField.STREET_ADDRESS) {
+      return address.getAddressLines().size() > 0;
+    } else {
+      String value = address.getFieldValue(field);
+      return (value != null && !value.isEmpty());
+    }
+  }
+
   private void applyFieldOrderOverrides(String regionCode, List<AddressField> fieldOrder) {
     List<AddressField> customFieldOrder = formOptions.getCustomFieldOrder(regionCode);
     if (customFieldOrder == null) {
@@ -194,7 +222,10 @@
   /**
    * Gets formatted address. For example,
    *
-   * <p> John Doe<br> Dnar Corp<br> 5th St<br> Santa Monica CA 90123 </p>
+   * <p> John Doe</br>
+   * Dnar Corp</br>
+   * 5th St</br>
+   * Santa Monica CA 90123 </p>
    *
    * This method does not validate addresses. Also, it will "normalize" the result strings by
    * removing redundant spaces and empty lines.
@@ -209,102 +240,127 @@
       scriptType = Util.isExplicitLatinScript(lc) ? ScriptType.LATIN : ScriptType.LOCAL;
     }
 
-    List<String> lines = new ArrayList<String>();
+    List<String> prunedFormat = new ArrayList<String>();
+    List<String> formatSubstrings = getFormatSubstrings(scriptType, regionCode);
+    for (int i = 0; i < formatSubstrings.size(); i++) {
+      String formatSubstring = formatSubstrings.get(i);
+      // Always keep the newlines.
+      if (formatSubstring.equals(NEW_LINE)) {
+        prunedFormat.add(NEW_LINE);
+      } else if (formatSubstringRepresentsField(formatSubstring)) {
+        // Always keep the non-empty address fields.
+        if (addressHasValueForField(address, getFieldForFormatSubstring(formatSubstring))) {
+          prunedFormat.add(formatSubstring);
+        }
+      } else if (
+          // Only keep literals that satisfy these 2 conditions:
+          // (1) Not preceding an empty field.
+          (i == formatSubstrings.size() - 1 || formatSubstrings.get(i + 1).equals(NEW_LINE)
+           || addressHasValueForField(address, getFieldForFormatSubstring(
+               formatSubstrings.get(i + 1))))
+          // (2) Not following a removed field.
+          && (i == 0 || !formatSubstringRepresentsField(formatSubstrings.get(i - 1))
+              || (!prunedFormat.isEmpty()
+                  && formatSubstringRepresentsField(prunedFormat.get(prunedFormat.size() - 1))))) {
+        prunedFormat.add(formatSubstring);
+      }
+    }
+
+    List<String> lines = new ArrayList<>();
     StringBuilder currentLine = new StringBuilder();
-    for (String formatSymbol : getFormatSubStrings(scriptType, regionCode)) {
-      if (formatSymbol.equals(NEW_LINE)) {
-        String normalizedStr = removeRedundantSpacesAndLeadingPunctuation(currentLine.toString());
-        if (normalizedStr.length() > 0) {
-          lines.add(normalizedStr);
+    for (String formatSubstring : prunedFormat) {
+      if (formatSubstring.equals(NEW_LINE)) {
+        if (currentLine.length() > 0) {
+          lines.add(currentLine.toString());
           currentLine.setLength(0);
         }
-      } else if (formatSymbol.startsWith("%")) {
-        String value = null;
-        switch (AddressField.of(formatSymbol.charAt(1))) {
+      } else if (formatSubstringRepresentsField(formatSubstring)) {
+        switch (getFieldForFormatSubstring(formatSubstring)) {
           case STREET_ADDRESS:
-            value =
-                Util.joinAndSkipNulls("\n", address.getAddressLine1(), address.getAddressLine2());
+            // The field "street address" represents the street address lines of an address, so
+            // there can be multiple values.
+            List<String> addressLines = address.getAddressLines();
+            if (addressLines.size() > 0) {
+              currentLine.append(addressLines.get(0));
+              if (addressLines.size() > 1) {
+                lines.add(currentLine.toString());
+                currentLine.setLength(0);
+                lines.addAll(addressLines.subList(1, addressLines.size()));
+              }
+            }
             break;
           case COUNTRY:
             // Country name is treated separately.
             break;
           case ADMIN_AREA:
-            value = address.getAdministrativeArea();
+            currentLine.append(address.getAdministrativeArea());
             break;
           case LOCALITY:
-            value = address.getLocality();
+            currentLine.append(address.getLocality());
             break;
           case DEPENDENT_LOCALITY:
-            value = address.getDependentLocality();
+            currentLine.append(address.getDependentLocality());
             break;
           case RECIPIENT:
-            value = address.getRecipient();
+            currentLine.append(address.getRecipient());
             break;
           case ORGANIZATION:
-            value = address.getOrganization();
+            currentLine.append(address.getOrganization());
             break;
           case POSTAL_CODE:
-            value = address.getPostalCode();
+            currentLine.append(address.getPostalCode());
+            break;
+          case SORTING_CODE:
+            currentLine.append(address.getSortingCode());
             break;
           default:
             break;
         }
-
-        if (value != null) {
-          currentLine.append(value);
-        }
       } else {
-        currentLine.append(formatSymbol);
+        // Not a symbol we recognise, so must be a literal. We append it unchanged.
+        currentLine.append(formatSubstring);
       }
     }
-    String normalizedStr = removeRedundantSpacesAndLeadingPunctuation(currentLine.toString());
-    if (normalizedStr.length() > 0) {
-      lines.add(normalizedStr);
+    if (currentLine.length() > 0) {
+      lines.add(currentLine.toString());
     }
     return lines;
   }
 
   /**
    * Tokenizes the format string and returns the token string list. "%" is treated as an escape
-   * character. So for example "%n%a%nxyz" will be split into "%n", "%a", "%n", "x", "y", and "z".
+   * character. For example, "%n%a%nxyz" will be split into "%n", "%a", "%n", "xyz".
    * Escaped tokens correspond to either new line or address fields. The output of this method
    * may contain duplicates.
    */
   // TODO: Create a common method which does field parsing in one place (there are about 4 other
   // places in this library where format strings are parsed).
-  private List<String> getFormatSubStrings(ScriptType scriptType, String regionCode) {
+  private List<String> getFormatSubstrings(ScriptType scriptType, String regionCode) {
     String formatString = getFormatString(scriptType, regionCode);
     List<String> parts = new ArrayList<String>();
 
     boolean escaped = false;
+    StringBuilder currentLiteral = new StringBuilder();
     for (char c : formatString.toCharArray()) {
       if (escaped) {
         escaped = false;
-        if (NEW_LINE.equals("%" + c)) {
-          parts.add(NEW_LINE);
-        } else {
-          // Checks that the character is valid.
-          AddressField.of(c);
-          parts.add("%" + c);
-        }
+        parts.add("%" + c);
       } else if (c == '%') {
+        if (currentLiteral.length() > 0) {
+          parts.add(currentLiteral.toString());
+          currentLiteral.setLength(0);
+        }
         escaped = true;
       } else {
-        parts.add(c + "");
+        currentLiteral.append(c);
       }
     }
+    if (currentLiteral.length() > 0) {
+      parts.add(currentLiteral.toString());
+    }
     return parts;
   }
 
-  private static String removeRedundantSpacesAndLeadingPunctuation(String str) {
-    // Remove leading commas and other punctuation that might have been added by the formatter
-    // in the case of missing data.
-    str = str.replaceFirst("^[-,\\s]+", "");
-    str = str.trim();
-    str = str.replaceAll(" +", " ");
-    return str;
-  }
-
   private static String getFormatString(ScriptType scriptType, String regionCode) {
     String format = (scriptType == ScriptType.LOCAL)
         ? getJsonValue(regionCode, AddressDataKey.FMT)
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
index dcc2a9e..10c2fc4 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
@@ -63,11 +63,11 @@
     map.put("BT", "{\"name\":\"BHUTAN\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
     map.put("BV", "{\"name\":\"BOUVET ISLAND\"}");
     map.put("BW", "{\"name\":\"BOTSWANA\"}");
-    map.put("BY", "{\"name\":\"BELARUS\",\"fmt\":\"%S%n%Z %C %X%n%A%n%O%n%N\"}");
+    map.put("BY", "{\"name\":\"BELARUS\",\"fmt\":\"%S%n%Z %C%n%A%n%O%n%N\"}");
     map.put("BZ", "{\"name\":\"BELIZE\"}");
     map.put("CA", "{\"name\":\"CANADA\",\"lang\":\"en\",\"languages\":\"en~fr\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOSZ\"}");
     map.put("CC", "{\"name\":\"COCOS (KEELING) ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}");
-    map.put("CD", "{\"name\":\"CONGO (DEM. REP.)\",\"fmt\":\"%N%n%O%n%A%n%C %X\"}");
+    map.put("CD", "{\"name\":\"CONGO (DEM. REP.)\"}");
     map.put("CF", "{\"name\":\"CENTRAL AFRICAN REPUBLIC\"}");
     map.put("CG", "{\"name\":\"CONGO (REP.)\"}");
     map.put("CH", "{\"name\":\"SWITZERLAND\",\"lang\":\"de\",\"languages\":\"de~fr~it\",\"fmt\":\"%O%n%N%n%A%nCH-%Z %C\",\"require\":\"ACZ\",\"upper\":\"\",\"postprefix\":\"CH-\"}");
@@ -98,7 +98,7 @@
     map.put("ET", "{\"name\":\"ETHIOPIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("FI", "{\"name\":\"FINLAND\",\"fmt\":\"%O%n%N%n%A%nFI-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"FI-\"}");
     map.put("FJ", "{\"name\":\"FIJI\"}");
-    map.put("FK", "{\"name\":\"FALKLAND ISLANDS (MALVINAS)\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("FK", "{\"name\":\"FALKLAND ISLANDS (MALVINAS)\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("FM", "{\"name\":\"MICRONESIA (Federated State of)\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
     map.put("FO", "{\"name\":\"FAROE ISLANDS\",\"fmt\":\"%N%n%O%n%A%nFO%Z %C\",\"postprefix\":\"FO\"}");
     map.put("FR", "{\"name\":\"FRANCE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"CX\"}");
@@ -107,7 +107,7 @@
     map.put("GD", "{\"name\":\"GRENADA (WEST INDIES)\"}");
     map.put("GE", "{\"name\":\"GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("GF", "{\"name\":\"FRENCH GUIANA\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
-    map.put("GG", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%nGUERNSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("GG", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%nGUERNSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("GH", "{\"name\":\"GHANA\"}");
     map.put("GI", "{\"name\":\"GIBRALTAR\",\"fmt\":\"%N%n%O%n%A%nGIBRALTAR%n%Z\",\"require\":\"A\"}");
     map.put("GL", "{\"name\":\"GREENLAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}");
@@ -116,7 +116,7 @@
     map.put("GP", "{\"name\":\"GUADELOUPE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
     map.put("GQ", "{\"name\":\"EQUATORIAL GUINEA\"}");
     map.put("GR", "{\"name\":\"GREECE\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}");
-    map.put("GS", "{\"name\":\"SOUTH GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("GS", "{\"name\":\"SOUTH GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("GT", "{\"name\":\"GUATEMALA\",\"fmt\":\"%N%n%O%n%A%n%Z- %C\"}");
     map.put("GU", "{\"name\":\"GUAM\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
     map.put("GW", "{\"name\":\"GUINEA-BISSAU\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
@@ -125,24 +125,24 @@
     map.put("HM", "{\"name\":\"HEARD AND MCDONALD ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}");
     map.put("HN", "{\"name\":\"HONDURAS\",\"fmt\":\"%N%n%O%n%A%n%C, %S%n%Z\",\"require\":\"ACS\"}");
     map.put("HR", "{\"name\":\"CROATIA\",\"fmt\":\"%N%n%O%n%A%nHR-%Z %C\",\"postprefix\":\"HR-\"}");
-    map.put("HT", "{\"name\":\"HAITI\",\"fmt\":\"%N%n%O%n%A%nHT%Z %C %X\",\"postprefix\":\"HT\"}");
+    map.put("HT", "{\"name\":\"HAITI\",\"fmt\":\"%N%n%O%n%A%nHT%Z %C\",\"postprefix\":\"HT\"}");
     map.put("HU", "{\"name\":\"HUNGARY (Rep.)\",\"fmt\":\"%N%n%O%n%C%n%A%n%Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\"}");
     map.put("ID", "{\"name\":\"INDONESIA\",\"lang\":\"id\",\"languages\":\"id\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"require\":\"AS\"}");
     map.put("IE", "{\"name\":\"IRELAND\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D%n%C%n%S %Z\",\"sublocality_name_type\":\"townland\",\"state_name_type\":\"county\"}");
     map.put("IL", "{\"name\":\"ISRAEL\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
-    map.put("IM", "{\"name\":\"ISLE OF MAN\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("IM", "{\"name\":\"ISLE OF MAN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("IN", "{\"name\":\"INDIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"require\":\"ACSZ\",\"state_name_type\":\"state\",\"zip_name_type\":\"pin\"}");
-    map.put("IO", "{\"name\":\"BRITISH INDIAN OCEAN TERRITORY\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("IO", "{\"name\":\"BRITISH INDIAN OCEAN TERRITORY\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("IQ", "{\"name\":\"IRAQ\",\"fmt\":\"%O%n%N%n%A%n%C, %S%n%Z\",\"require\":\"ACS\",\"upper\":\"CS\"}");
     map.put("IR", "{\"name\":\"IRAN\",\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\",\"sublocality_name_type\":\"neighborhood\"}");
     map.put("IS", "{\"name\":\"ICELAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("IT", "{\"name\":\"ITALY\",\"lang\":\"it\",\"languages\":\"it\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}");
-    map.put("JE", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%nJERSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("JE", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%nJERSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("JM", "{\"name\":\"JAMAICA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %X\",\"require\":\"ACS\",\"state_name_type\":\"parish\"}");
     map.put("JO", "{\"name\":\"JORDAN\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
     map.put("JP", "{\"name\":\"JAPAN\",\"lang\":\"ja\",\"languages\":\"ja\",\"lfmt\":\"%N%n%O%n%A%n%C, %S%n%Z\",\"fmt\":\"〒%Z%n%S%C%n%A%n%O%n%N\",\"require\":\"ACSZ\",\"upper\":\"S\",\"state_name_type\":\"prefecture\",\"width_overrides\":\"%S:S\"}");
     map.put("KE", "{\"name\":\"KENYA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}");
-    map.put("KG", "{\"name\":\"KYRGYZSTAN\",\"fmt\":\"%Z %C %X%n%A%n%O%n%N\"}");
+    map.put("KG", "{\"name\":\"KYRGYZSTAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("KH", "{\"name\":\"CAMBODIA\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
     map.put("KI", "{\"name\":\"KIRIBATI\",\"fmt\":\"%N%n%O%n%A%n%S%n%C\",\"upper\":\"ACNOS\",\"state_name_type\":\"island\"}");
     map.put("KM", "{\"name\":\"COMOROS\",\"upper\":\"AC\"}");
@@ -156,7 +156,7 @@
     map.put("LC", "{\"name\":\"SAINT LUCIA\"}");
     map.put("LI", "{\"name\":\"LIECHTENSTEIN\",\"fmt\":\"%O%n%N%n%A%nFL-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"FL-\"}");
     map.put("LK", "{\"name\":\"SRI LANKA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}");
-    map.put("LR", "{\"name\":\"LIBERIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C %X\"}");
+    map.put("LR", "{\"name\":\"LIBERIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("LS", "{\"name\":\"LESOTHO\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
     map.put("LT", "{\"name\":\"LITHUANIA\",\"fmt\":\"%O%n%N%n%A%nLT-%Z %C\",\"postprefix\":\"LT-\"}");
     map.put("LU", "{\"name\":\"LUXEMBOURG\",\"fmt\":\"%O%n%N%n%A%nL-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"L-\"}");
@@ -172,7 +172,7 @@
     map.put("MK", "{\"name\":\"MACEDONIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("ML", "{\"name\":\"MALI\"}");
     map.put("MM", "{\"name\":\"MYANMAR\",\"fmt\":\"%N%n%O%n%A%n%C, %Z\"}");
-    map.put("MN", "{\"name\":\"MONGOLIA\",\"fmt\":\"%N%n%O%n%A%n%S %C-%X%n%Z\"}");
+    map.put("MN", "{\"name\":\"MONGOLIA\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\"}");
     map.put("MO", "{\"name\":\"MACAO\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant\",\"lfmt\":\"%N%n%O%n%A\",\"fmt\":\"%A%n%O%n%N\",\"require\":\"A\"}");
     map.put("MP", "{\"name\":\"NORTHERN MARIANA ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
     map.put("MQ", "{\"name\":\"MARTINIQUE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
@@ -206,7 +206,7 @@
     map.put("PK", "{\"name\":\"PAKISTAN\",\"fmt\":\"%N%n%O%n%A%n%C-%Z\"}");
     map.put("PL", "{\"name\":\"POLAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}");
     map.put("PM", "{\"name\":\"ST. PIERRE AND MIQUELON\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
-    map.put("PN", "{\"name\":\"PITCAIRN\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("PN", "{\"name\":\"PITCAIRN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("PR", "{\"name\":\"PUERTO RICO\",\"fmt\":\"%N%n%O%n%A%n%C PR %Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\",\"zip_name_type\":\"zip\",\"postprefix\":\"PR\"}");
     map.put("PS", "{\"name\":\"PALESTINIAN TERRITORY\"}");
     map.put("PT", "{\"name\":\"PORTUGAL\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}");
@@ -223,7 +223,7 @@
     map.put("SC", "{\"name\":\"SEYCHELLES\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"S\",\"state_name_type\":\"island\"}");
     map.put("SE", "{\"name\":\"SWEDEN\",\"fmt\":\"%O%n%N%n%A%nSE-%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\",\"postprefix\":\"SE-\"}");
     map.put("SG", "{\"name\":\"REP. OF SINGAPORE\",\"fmt\":\"%N%n%O%n%A%nSINGAPORE %Z\",\"require\":\"AZ\"}");
-    map.put("SH", "{\"name\":\"SAINT HELENA\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("SH", "{\"name\":\"SAINT HELENA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("SI", "{\"name\":\"SLOVENIA\",\"fmt\":\"%N%n%O%n%A%nSI- %Z %C\",\"postprefix\":\"SI-\"}");
     map.put("SJ", "{\"name\":\"SVALBARD AND JAN MAYEN ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\"}");
     map.put("SK", "{\"name\":\"SLOVAKIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
@@ -231,14 +231,14 @@
     map.put("SM", "{\"name\":\"SAN MARINO\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"AZ\"}");
     map.put("SN", "{\"name\":\"SENEGAL\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("SO", "{\"name\":\"SOMALIA\",\"lang\":\"so\",\"languages\":\"so\",\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"require\":\"ACS\",\"upper\":\"ACS\"}");
-    map.put("SR", "{\"name\":\"SURINAME\",\"lang\":\"nl\",\"languages\":\"nl\",\"fmt\":\"%N%n%O%n%A%n%C %X%n%S\",\"upper\":\"AS\"}");
+    map.put("SR", "{\"name\":\"SURINAME\",\"lang\":\"nl\",\"languages\":\"nl\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"AS\"}");
     map.put("SS", "{\"name\":\"SOUTH SUDAN\"}");
-    map.put("ST", "{\"name\":\"SAO TOME AND PRINCIPE\",\"fmt\":\"%N%n%O%n%A%n%C %X\"}");
+    map.put("ST", "{\"name\":\"SAO TOME AND PRINCIPE\"}");
     map.put("SV", "{\"name\":\"EL SALVADOR\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z-%C%n%S\",\"require\":\"ACS\",\"upper\":\"CSZ\"}");
     map.put("SX", "{\"name\":\"SINT MAARTEN\"}");
     map.put("SZ", "{\"name\":\"SWAZILAND\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"upper\":\"ACZ\"}");
     map.put("TA", "{\"name\":\"TRISTAN DA CUNHA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}");
-    map.put("TC", "{\"name\":\"TURKS AND CAICOS ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
+    map.put("TC", "{\"name\":\"TURKS AND CAICOS ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("TD", "{\"name\":\"CHAD\"}");
     map.put("TF", "{\"name\":\"FRENCH SOUTHERN TERRITORIES\"}");
     map.put("TG", "{\"name\":\"TOGO\"}");
@@ -251,7 +251,7 @@
     map.put("TO", "{\"name\":\"TONGA\"}");
     map.put("TR", "{\"name\":\"TURKEY\",\"lang\":\"tr\",\"languages\":\"tr\",\"fmt\":\"%N%n%O%n%A%n%Z %C/%S\",\"require\":\"ACZ\",\"locality_name_type\":\"district\"}");
     map.put("TT", "{\"name\":\"TRINIDAD AND TOBAGO\"}");
-    map.put("TV", "{\"name\":\"TUVALU\",\"lang\":\"tyv\",\"languages\":\"tyv\",\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%S\",\"upper\":\"ACS\",\"state_name_type\":\"island\"}");
+    map.put("TV", "{\"name\":\"TUVALU\",\"lang\":\"tyv\",\"languages\":\"tyv\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"ACS\",\"state_name_type\":\"island\"}");
     map.put("TW", "{\"name\":\"TAIWAN\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant\",\"lfmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"fmt\":\"%Z%n%S%C%n%A%n%O%n%N\",\"require\":\"ACSZ\",\"state_name_type\":\"county\"}");
     map.put("TZ", "{\"name\":\"TANZANIA (UNITED REP.)\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("UA", "{\"name\":\"UKRAINE\",\"lang\":\"uk\",\"languages\":\"uk\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"require\":\"ACSZ\",\"state_name_type\":\"oblast\"}");
diff --git a/common/src/test/java/com/google/i18n/addressinput/common/FormatInterpreterTest.java b/common/src/test/java/com/google/i18n/addressinput/common/FormatInterpreterTest.java
index 4e2091e..1750f63 100644
--- a/common/src/test/java/com/google/i18n/addressinput/common/FormatInterpreterTest.java
+++ b/common/src/test/java/com/google/i18n/addressinput/common/FormatInterpreterTest.java
@@ -26,6 +26,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -97,6 +98,99 @@
         .inOrder();
   }
 
+  @Test public void testGetEnvelopeAddress_MissingFields_LiteralsBetweenFields() {
+    FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
+    AddressData.Builder addressBuilder = AddressData.builder()
+        .setCountry("US");
+
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build())).isEmpty();
+
+    addressBuilder.setAdminArea("CA");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("CA");
+
+    addressBuilder.setLocality("Los Angeles");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("Los Angeles, CA");
+
+    addressBuilder.setPostalCode("90291");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("Los Angeles, CA 90291");
+
+    addressBuilder.setAdminArea("");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("Los Angeles 90291");
+
+    addressBuilder.setLocality("");
+    addressBuilder.setAdminArea("CA");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("CA 90291");
+  }
+
+  @Test public void testGetEnvelopeAddress_MissingFields_LiteralsOnSeparateLine() {
+    FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
+    AddressData.Builder addressBuilder = AddressData.builder()
+        .setCountry("AX");
+
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("ÅLAND");
+
+    addressBuilder.setLocality("City");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("City", "ÅLAND").inOrder();
+
+    addressBuilder.setPostalCode("123");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("AX-123 City", "ÅLAND").inOrder();
+  }
+
+  @Test public void testGetEnvelopeAddress_MissingFields_LiteralBeforeField() {
+    FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
+    AddressData.Builder addressBuilder = AddressData.builder()
+        .setCountry("JP")
+        .setLanguageCode("ja");
+
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build())).isEmpty();
+
+    addressBuilder.setPostalCode("123");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("〒123");
+
+    addressBuilder.setAdminArea("Prefecture");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("〒123", "Prefecture").inOrder();
+
+    addressBuilder.setPostalCode("");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("Prefecture");
+  }
+
+  @Test public void testGetEnvelopeAddress_MissingFields_DuplicateField() {
+    FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
+    AddressData.Builder addressBuilder = AddressData.builder()
+        .setCountry("CI");
+
+    addressBuilder.setSortingCode("123");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("123 123");
+
+    addressBuilder.setAddressLine1("456 Main St");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("123 456 Main St 123");
+
+    addressBuilder.setLocality("Yamoussoukro");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("123 456 Main St Yamoussoukro 123");
+
+    addressBuilder.setSortingCode("");
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("456 Main St Yamoussoukro");
+
+    addressBuilder.setAddressLines(new ArrayList<String>());
+    assertThat(formatInterpreter.getEnvelopeAddress(addressBuilder.build()))
+        .containsExactly("Yamoussoukro");
+  }
+
   @Test public void testUsEnvelopeAddress() {
     FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
     AddressData address = AddressData.builder()
@@ -132,7 +226,7 @@
         .inOrder();
   }
 
-  @Test public void testEnvelopeAddressIncompleteAddress() {
+  @Test public void testGetEnvelopeAddressIncompleteAddress() {
     FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
     AddressData address = AddressData.builder()
         .setCountry("US")
@@ -145,13 +239,13 @@
         .containsExactly("1098 Alta Ave", "CA 94043").inOrder();
   }
 
-  @Test public void testEnvelopeAddressEmptyAddress() {
+  @Test public void testGetEnvelopeAddressEmptyAddress() {
     FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
     AddressData address = AddressData.builder().setCountry("US").build();
     assertThat(formatInterpreter.getEnvelopeAddress(address)).isEmpty();
   }
 
-  @Test public void testEnvelopeAddressLeadingPostPrefix() {
+  @Test public void testGetEnvelopeAddressLeadingPostPrefix() {
     FormatInterpreter formatInterpreter = new FormatInterpreter(new FormOptions().createSnapshot());
     AddressData address = AddressData.builder()
         .setCountry("CH")
diff --git a/cpp/src/region_data_constants.cc b/cpp/src/region_data_constants.cc
index 0afb6de..7791660 100644
--- a/cpp/src/region_data_constants.cc
+++ b/cpp/src/region_data_constants.cc
@@ -231,7 +231,7 @@
       "\"languages\":\"en~tn\""
       "}"));
   region_data.insert(std::make_pair("BY", "{"
-      "\"fmt\":\"%S%n%Z %C %X%n%A%n%O%n%N\","
+      "\"fmt\":\"%S%n%Z %C%n%A%n%O%n%N\","
       "\"zipex\":\"223016,225860,220050\","
       "\"posturl\":\"http://zip.belpost.by\","
       "\"languages\":\"be~ru\""
@@ -252,7 +252,6 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("CD", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%C %X\","
       "\"languages\":\"fr\""
       "}"));
   region_data.insert(std::make_pair("CF", "{"
@@ -412,7 +411,7 @@
       "\"languages\":\"en~hif-Latn~fj\""
       "}"));
   region_data.insert(std::make_pair("FK", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"FIQQ 1ZZ\","
       "\"languages\":\"en\""
@@ -467,7 +466,7 @@
       "\"languages\":\"fr\""
       "}"));
   region_data.insert(std::make_pair("GG", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%nGUERNSEY%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%nGUERNSEY%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"GY1 1AA,GY2 2BT\","
       "\"posturl\":\"http://www.guernseypost.com/postcode_finder/\","
@@ -514,7 +513,7 @@
       "\"languages\":\"el\""
       "}"));
   region_data.insert(std::make_pair("GS", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"SIQQ 1ZZ\""
       "}"));
@@ -565,7 +564,7 @@
       "\"languages\":\"hr\""
       "}"));
   region_data.insert(std::make_pair("HT", "{"
-      "\"fmt\":\"%N%n%O%n%A%nHT%Z %C %X\","
+      "\"fmt\":\"%N%n%O%n%A%nHT%Z %C\","
       "\"zipex\":\"6120,5310,6110,8510\","
       "\"languages\":\"ht~fr\""
       "}"));
@@ -597,7 +596,7 @@
       "\"languages\":\"iw~ar\""
       "}"));
   region_data.insert(std::make_pair("IM", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"IM2 1AA,IM99 1PS\","
       "\"posturl\":\"http://www.gov.im/post/postal/fr_main.asp\","
@@ -613,7 +612,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("IO", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"BBND 1ZZ\","
       "\"languages\":\"en\""
@@ -644,7 +643,7 @@
       "\"languages\":\"it\""
       "}"));
   region_data.insert(std::make_pair("JE", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%nJERSEY%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%nJERSEY%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"JE1 1AA,JE2 2BT\","
       "\"posturl\":\"http://www.jerseypost.com/tools/postcode-address-finder/\","
@@ -676,7 +675,7 @@
       "\"languages\":\"sw~en\""
       "}"));
   region_data.insert(std::make_pair("KG", "{"
-      "\"fmt\":\"%Z %C %X%n%A%n%O%n%N\","
+      "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"720001\","
       "\"languages\":\"ky-Cyrl~ru\""
       "}"));
@@ -754,7 +753,7 @@
       "\"languages\":\"si~ta\""
       "}"));
   region_data.insert(std::make_pair("LR", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%Z %C %X\","
+      "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"1000\","
       "\"languages\":\"en\""
       "}"));
@@ -840,7 +839,7 @@
       "\"languages\":\"my\""
       "}"));
   region_data.insert(std::make_pair("MN", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%S %C-%X%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\","
       "\"zipex\":\"65030,65270\","
       "\"posturl\":\"http://www.zipcode.mn/\","
       "\"languages\":\"mn-Cyrl\""
@@ -1042,7 +1041,7 @@
       "\"languages\":\"fr\""
       "}"));
   region_data.insert(std::make_pair("PN", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"PCRN 1ZZ\","
       "\"languages\":\"en\""
@@ -1142,7 +1141,7 @@
       "\"languages\":\"en~zh-Hans~ms-Latn~ta\""
       "}"));
   region_data.insert(std::make_pair("SH", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"STHL 1ZZ\","
       "\"languages\":\"en\""
@@ -1188,14 +1187,13 @@
       "\"languages\":\"so\""
       "}"));
   region_data.insert(std::make_pair("SR", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%C %X%n%S\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%S\","
       "\"languages\":\"nl\""
       "}"));
   region_data.insert(std::make_pair("SS", "{"
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("ST", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%C %X\","
       "\"languages\":\"pt\""
       "}"));
   region_data.insert(std::make_pair("SV", "{"
@@ -1219,7 +1217,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("TC", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%Z\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"require\":\"ACZ\","
       "\"zipex\":\"TKCA 1ZZ\","
       "\"languages\":\"en\""
@@ -1276,7 +1274,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("TV", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%X%n%C%n%S\","
+      "\"fmt\":\"%N%n%O%n%A%n%C%n%S\","
       "\"state_name_type\":\"island\","
       "\"languages\":\"tyv\""
       "}"));