content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed 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.
   */
  
  package android.webkit;
  
  import java.util.Locale;
  import java.util.regex.MatchResult;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Java implementation of legacy WebView.findAddress algorithm.
   *
   * @hide
   */
  class FindAddress {
      static class ZipRange {
          int mLow;
          int mHigh;
          int mException1;
          int mException2;
          ZipRange(int low, int high, int exception1, int exception2) {
              mLow = low;
              mHigh = high;
              mException1 = exception1;
              mException2 = exception1;
          }
          boolean matches(String zipCode) {
              int prefix = Integer.parseInt(zipCode.substring(0, 2));
              return (mLow <= prefix && prefix <= mHigh) || prefix == mException1
                      || prefix == mException2;
          }
      }
  
      // Addresses consist of at least this many words, not including state and zip code.
      private static final int MIN_ADDRESS_WORDS = 4;
  
      // Adddresses consist of at most this many words, not including state and zip code.
      private static final int MAX_ADDRESS_WORDS = 14;
  
      // Addresses consist of at most this many lines.
      private static final int MAX_ADDRESS_LINES = 5;
  
      // No words in an address are longer than this many characters.
      private static final int kMaxAddressNameWordLength = 25;
  
      // Location name should be in the first MAX_LOCATION_NAME_DISTANCE words
      private static final int MAX_LOCATION_NAME_DISTANCE = 5;
  
      private static final ZipRange[] sStateZipCodeRanges = {
              new ZipRange(99, 99, -1, -1), // AK Alaska.
              new ZipRange(35, 36, -1, -1), // AL Alabama.
              new ZipRange(71, 72, -1, -1), // AR Arkansas.
              new ZipRange(96, 96, -1, -1), // AS American Samoa.
              new ZipRange(85, 86, -1, -1), // AZ Arizona.
              new ZipRange(90, 96, -1, -1), // CA California.
              new ZipRange(80, 81, -1, -1), // CO Colorado.
              new ZipRange(6, 6, -1, -1), // CT Connecticut.
              new ZipRange(20, 20, -1, -1), // DC District of Columbia.
              new ZipRange(19, 19, -1, -1), // DE Delaware.
              new ZipRange(32, 34, -1, -1), // FL Florida.
              new ZipRange(96, 96, -1, -1), // FM Federated States of Micronesia.
              new ZipRange(30, 31, -1, -1), // GA Georgia.
              new ZipRange(96, 96, -1, -1), // GU Guam.
              new ZipRange(96, 96, -1, -1), // HI Hawaii.
              new ZipRange(50, 52, -1, -1), // IA Iowa.
              new ZipRange(83, 83, -1, -1), // ID Idaho.
              new ZipRange(60, 62, -1, -1), // IL Illinois.
              new ZipRange(46, 47, -1, -1), // IN Indiana.
              new ZipRange(66, 67, 73, -1), // KS Kansas.
              new ZipRange(40, 42, -1, -1), // KY Kentucky.
              new ZipRange(70, 71, -1, -1), // LA Louisiana.
              new ZipRange(1, 2, -1, -1), // MA Massachusetts.
              new ZipRange(20, 21, -1, -1), // MD Maryland.
              new ZipRange(3, 4, -1, -1), // ME Maine.
              new ZipRange(96, 96, -1, -1), // MH Marshall Islands.
              new ZipRange(48, 49, -1, -1), // MI Michigan.
              new ZipRange(55, 56, -1, -1), // MN Minnesota.
              new ZipRange(63, 65, -1, -1), // MO Missouri.
              new ZipRange(96, 96, -1, -1), // MP Northern Mariana Islands.
              new ZipRange(38, 39, -1, -1), // MS Mississippi.
              new ZipRange(55, 56, -1, -1), // MT Montana.
              new ZipRange(27, 28, -1, -1), // NC North Carolina.
              new ZipRange(58, 58, -1, -1), // ND North Dakota.
              new ZipRange(68, 69, -1, -1), // NE Nebraska.
              new ZipRange(3, 4, -1, -1), // NH New Hampshire.
              new ZipRange(7, 8, -1, -1), // NJ New Jersey.
              new ZipRange(87, 88, 86, -1), // NM New Mexico.
              new ZipRange(88, 89, 96, -1), // NV Nevada.
              new ZipRange(10, 14, 0, 6), // NY New York.
              new ZipRange(43, 45, -1, -1), // OH Ohio.
              new ZipRange(73, 74, -1, -1), // OK Oklahoma.
              new ZipRange(97, 97, -1, -1), // OR Oregon.
              new ZipRange(15, 19, -1, -1), // PA Pennsylvania.
              new ZipRange(6, 6, 0, 9), // PR Puerto Rico.
              new ZipRange(96, 96, -1, -1), // PW Palau.
              new ZipRange(2, 2, -1, -1), // RI Rhode Island.
              new ZipRange(29, 29, -1, -1), // SC South Carolina.
              new ZipRange(57, 57, -1, -1), // SD South Dakota.
              new ZipRange(37, 38, -1, -1), // TN Tennessee.
              new ZipRange(75, 79, 87, 88), // TX Texas.
              new ZipRange(84, 84, -1, -1), // UT Utah.
              new ZipRange(22, 24, 20, -1), // VA Virginia.
              new ZipRange(6, 9, -1, -1), // VI Virgin Islands.
              new ZipRange(5, 5, -1, -1), // VT Vermont.
              new ZipRange(98, 99, -1, -1), // WA Washington.
              new ZipRange(53, 54, -1, -1), // WI Wisconsin.
              new ZipRange(24, 26, -1, -1), // WV West Virginia.
              new ZipRange(82, 83, -1, -1) // WY Wyoming.
      };
  
      // Newlines
      private static final String NL = "\n\u000B\u000C\r\u0085\u2028\u2029";
  
      // Space characters
      private static final String SP = "\u0009\u0020\u00A0\u1680\u2000\u2001"
              + "\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F"
              + "\u205F\u3000";
  
      // Whitespace
      private static final String WS = SP + NL;
  
      // Characters that are considered word delimiters.
      private static final String WORD_DELIM = ",*\u2022" + WS;
  
      // Lookahead for word end.
      private static final String WORD_END = "(?=[" + WORD_DELIM + "]|$)";
  
      // Address words are a sequence of non-delimiter characters.
      private static final Pattern sWordRe =
              Pattern.compile("[^" + WORD_DELIM + "]+" + WORD_END, Pattern.CASE_INSENSITIVE);
  
      // Characters that are considered suffix delimiters for house numbers.
      private static final String HOUSE_POST_DELIM = ",\"'" + WS;
  
      // Lookahead for house end.
      private static final String HOUSE_END = "(?=[" + HOUSE_POST_DELIM + "]|$)";
  
      // Characters that are considered prefix delimiters for house numbers.
      private static final String HOUSE_PRE_DELIM = ":" + HOUSE_POST_DELIM;
  
      // A house number component is "one" or a number, optionally
      // followed by a single alphabetic character, or
      private static final String HOUSE_COMPONENT = "(?:one|\\d+([a-z](?=[^a-z]|$)|st|nd|rd|th)?)";
  
      // House numbers are a repetition of |HOUSE_COMPONENT|, separated by -, and followed by
      // a delimiter character.
      private static final Pattern sHouseNumberRe =
              Pattern.compile(HOUSE_COMPONENT + "(?:-" + HOUSE_COMPONENT + ")*" + HOUSE_END,
                      Pattern.CASE_INSENSITIVE);
  
      // XXX: do we want to accept whitespace other than 0x20 in state names?
      private static final Pattern sStateRe = Pattern.compile("(?:"
                      + "(ak|alaska)|"
                      + "(al|alabama)|"
                      + "(ar|arkansas)|"
                      + "(as|american[" + SP + "]+samoa)|"
                      + "(az|arizona)|"
                      + "(ca|california)|"
                      + "(co|colorado)|"
                      + "(ct|connecticut)|"
                      + "(dc|district[" + SP + "]+of[" + SP + "]+columbia)|"
                      + "(de|delaware)|"
                      + "(fl|florida)|"
                      + "(fm|federated[" + SP + "]+states[" + SP + "]+of[" + SP + "]+micronesia)|"
                      + "(ga|georgia)|"
                      + "(gu|guam)|"
                      + "(hi|hawaii)|"
                      + "(ia|iowa)|"
                      + "(id|idaho)|"
                      + "(il|illinois)|"
                      + "(in|indiana)|"
                      + "(ks|kansas)|"
                      + "(ky|kentucky)|"
                      + "(la|louisiana)|"
                      + "(ma|massachusetts)|"
                      + "(md|maryland)|"
                      + "(me|maine)|"
                      + "(mh|marshall[" + SP + "]+islands)|"
                      + "(mi|michigan)|"
                      + "(mn|minnesota)|"
                      + "(mo|missouri)|"
                      + "(mp|northern[" + SP + "]+mariana[" + SP + "]+islands)|"
                      + "(ms|mississippi)|"
                      + "(mt|montana)|"
                      + "(nc|north[" + SP + "]+carolina)|"
                      + "(nd|north[" + SP + "]+dakota)|"
                      + "(ne|nebraska)|"
                      + "(nh|new[" + SP + "]+hampshire)|"
                      + "(nj|new[" + SP + "]+jersey)|"
                      + "(nm|new[" + SP + "]+mexico)|"
                      + "(nv|nevada)|"
                      + "(ny|new[" + SP + "]+york)|"
                      + "(oh|ohio)|"
                      + "(ok|oklahoma)|"
                      + "(or|oregon)|"
                      + "(pa|pennsylvania)|"
                      + "(pr|puerto[" + SP + "]+rico)|"
                      + "(pw|palau)|"
                      + "(ri|rhode[" + SP + "]+island)|"
                      + "(sc|south[" + SP + "]+carolina)|"
                      + "(sd|south[" + SP + "]+dakota)|"
                      + "(tn|tennessee)|"
                      + "(tx|texas)|"
                      + "(ut|utah)|"
                      + "(va|virginia)|"
                      + "(vi|virgin[" + SP + "]+islands)|"
                      + "(vt|vermont)|"
                      + "(wa|washington)|"
                      + "(wi|wisconsin)|"
                      + "(wv|west[" + SP + "]+virginia)|"
                      + "(wy|wyoming)"
                      + ")" + WORD_END,
              Pattern.CASE_INSENSITIVE);
  
      private static final Pattern sLocationNameRe = Pattern.compile("(?:"
                      + "alley|annex|arcade|ave[.]?|avenue|alameda|bayou|"
                      + "beach|bend|bluffs?|bottom|boulevard|branch|bridge|"
                      + "brooks?|burgs?|bypass|broadway|camino|camp|canyon|"
                      + "cape|causeway|centers?|circles?|cliffs?|club|common|"
                      + "corners?|course|courts?|coves?|creek|crescent|crest|"
                      + "crossing|crossroad|curve|circulo|dale|dam|divide|"
                      + "drives?|estates?|expressway|extensions?|falls?|ferry|"
                      + "fields?|flats?|fords?|forest|forges?|forks?|fort|"
                      + "freeway|gardens?|gateway|glens?|greens?|groves?|"
                      + "harbors?|haven|heights|highway|hills?|hollow|inlet|"
                      + "islands?|isle|junctions?|keys?|knolls?|lakes?|land|"
                      + "landing|lane|lights?|loaf|locks?|lodge|loop|mall|"
                      + "manors?|meadows?|mews|mills?|mission|motorway|mount|"
                      + "mountains?|neck|orchard|oval|overpass|parks?|"
                      + "parkways?|pass|passage|path|pike|pines?|plains?|"
                      + "plaza|points?|ports?|prairie|privada|radial|ramp|"
                      + "ranch|rapids?|rd[.]?|rest|ridges?|river|roads?|route|"
                      + "row|rue|run|shoals?|shores?|skyway|springs?|spurs?|"
                      + "squares?|station|stravenue|stream|st[.]?|streets?|"
                      + "summit|speedway|terrace|throughway|trace|track|"
                      + "trafficway|trail|tunnel|turnpike|underpass|unions?|"
                      + "valleys?|viaduct|views?|villages?|ville|vista|walks?|"
                      + "wall|ways?|wells?|xing|xrd)" + WORD_END,
              Pattern.CASE_INSENSITIVE);
  
      private static final Pattern sSuffixedNumberRe =
              Pattern.compile("(\\d+)(st|nd|rd|th)", Pattern.CASE_INSENSITIVE);
  
      private static final Pattern sZipCodeRe =
              Pattern.compile("(?:\\d{5}(?:-\\d{4})?)" + WORD_END, Pattern.CASE_INSENSITIVE);
  
      private static boolean checkHouseNumber(String houseNumber) {
          // Make sure that there are at most 5 digits.
          int digitCount = 0;
          for (int i = 0; i < houseNumber.length(); ++i) {
              if (Character.isDigit(houseNumber.charAt(i))) ++digitCount;
          }
          if (digitCount > 5) return false;
  
          // Make sure that any ordinals are valid.
          Matcher suffixMatcher = sSuffixedNumberRe.matcher(houseNumber);
          while (suffixMatcher.find()) {
              int num = Integer.parseInt(suffixMatcher.group(1));
              if (num == 0) {
                  return false; // 0th is invalid.
              }
              String suffix = suffixMatcher.group(2).toLowerCase(Locale.getDefault());
              switch (num % 10) {
                  case 1:
                      return suffix.equals(num % 100 == 11 ? "th" : "st");
                  case 2:
                      return suffix.equals(num % 100 == 12 ? "th" : "nd");
                  case 3:
                      return suffix.equals(num % 100 == 13 ? "th" : "rd");
                  default:
                      return suffix.equals("th");
              }
          }
          return true;
      }
  
      /**
       * Attempt to match a house number beginnning at position offset
       * in content.  The house number must be followed by a word
       * delimiter or the end of the string, and if offset is non-zero,
       * then it must also be preceded by a word delimiter.
       *
       * @return a MatchResult if a valid house number was found.
       */
      private static MatchResult matchHouseNumber(String content, int offset) {
          if (offset > 0 && HOUSE_PRE_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
          Matcher matcher = sHouseNumberRe.matcher(content).region(offset, content.length());
          if (matcher.lookingAt()) {
              MatchResult matchResult = matcher.toMatchResult();
              if (checkHouseNumber(matchResult.group(0))) return matchResult;
          }
          return null;
      }
  
      /**
       * Attempt to match a US state beginnning at position offset in
       * content.  The matching state must be followed by a word
       * delimiter or the end of the string, and if offset is non-zero,
       * then it must also be preceded by a word delimiter.
       *
       * @return a MatchResult if a valid US state (or two letter code)
       * was found.
       */
      private static MatchResult matchState(String content, int offset) {
          if (offset > 0 && WORD_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
          Matcher stateMatcher = sStateRe.matcher(content).region(offset, content.length());
          return stateMatcher.lookingAt() ? stateMatcher.toMatchResult() : null;
      }
  
      /**
       * Test whether zipCode matches the U.S. zip code format (ddddd or
       * ddddd-dddd) and is within the expected range, given that
       * stateMatch is a match of sStateRe.
       *
       * @return true if zipCode is a valid zip code, is legal for the
       * matched state, and is followed by a word delimiter or the end
       * of the string.
       */
      private static boolean isValidZipCode(String zipCode, MatchResult stateMatch) {
          if (stateMatch == null) return false;
          // Work out the index of the state, based on which group matched.
          int stateIndex = stateMatch.groupCount();
          while (stateIndex > 0) {
              if (stateMatch.group(stateIndex--) != null) break;
          }
          return sZipCodeRe.matcher(zipCode).matches()
                  && sStateZipCodeRanges[stateIndex].matches(zipCode);
      }
  
      /**
       * Test whether location is one of the valid locations.
       *
       * @return true if location starts with a valid location name
       * followed by a word delimiter or the end of the string.
       */
      private static boolean isValidLocationName(String location) {
          return sLocationNameRe.matcher(location).matches();
      }
  
      /**
       * Attempt to match a complete address in content, starting with
       * houseNumberMatch.
       *
       * @param content The string to search.
       * @param houseNumberMatch A matching house number to start extending.
       * @return +ve: the end of the match
       *         +ve: the position to restart searching for house numbers, negated.
       */
      private static int attemptMatch(String content, MatchResult houseNumberMatch) {
          int restartPos = -1;
          int nonZipMatch = -1;
          int it = houseNumberMatch.end();
          int numLines = 1;
          boolean consecutiveHouseNumbers = true;
          boolean foundLocationName = false;
          int wordCount = 1;
          String lastWord = "";
  
          Matcher matcher = sWordRe.matcher(content);
  
          for (; it < content.length(); lastWord = matcher.group(0), it = matcher.end()) {
              if (!matcher.find(it)) {
                  // No more words in the input sequence.
                  return -content.length();
              }
              if (matcher.end() - matcher.start() > kMaxAddressNameWordLength) {
                  // Word is too long to be part of an address. Fail.
                  return -matcher.end();
              }
  
              // Count the number of newlines we just consumed.
              while (it < matcher.start()) {
                  if (NL.indexOf(content.charAt(it++)) != -1) ++numLines;
              }
  
              // Consumed too many lines. Fail.
              if (numLines > MAX_ADDRESS_LINES) break;
  
              // Consumed too many words. Fail.
              if (++wordCount > MAX_ADDRESS_WORDS) break;
  
              if (matchHouseNumber(content, it) != null) {
                  if (consecutiveHouseNumbers && numLines > 1) {
                      // Last line ended with a number, and this this line starts with one.
                      // Restart at this number.
                      return -it;
                  }
                  // Remember the position of this match as the restart position.
                  if (restartPos == -1) restartPos = it;
                  continue;
              }
  
              consecutiveHouseNumbers = false;
  
              if (isValidLocationName(matcher.group(0))) {
                  foundLocationName = true;
                  continue;
              }
  
              if (wordCount == MAX_LOCATION_NAME_DISTANCE && !foundLocationName) {
                  // Didn't find a location name in time. Fail.
                  it = matcher.end();
                  break;
              }
  
              if (foundLocationName && wordCount > MIN_ADDRESS_WORDS) {
                  // We can now attempt to match a state.
                  MatchResult stateMatch = matchState(content, it);
                  if (stateMatch != null) {
                      if (lastWord.equals("et") && stateMatch.group(0).equals("al")) {
                          // Reject "et al" as a false postitive.
                          it = stateMatch.end();
                          break;
                      }
  
                      // At this point we've matched a state; try to match a zip code after it.
                      Matcher zipMatcher = sWordRe.matcher(content);
                      if (zipMatcher.find(stateMatch.end())) {
                          if (isValidZipCode(zipMatcher.group(0), stateMatch)) {
                              return zipMatcher.end();
                          }
                      } else {
                          // The content ends with a state but no zip
                          // code. This is a legal match according to the
                          // documentation. N.B. This is equivalent to the
                          // original c++ implementation, which only allowed
                          // the zip code to be optional at the end of the
                          // string, which presumably is a bug.  We tried
                          // relaxing this to work in other places but it
                          // caused too many false positives.
                          nonZipMatch = stateMatch.end();
                      }
                  }
              }
          }
  
          if (nonZipMatch > 0) return nonZipMatch;
  
          return -(restartPos > 0 ? restartPos : it);
      }
  
      /**
       * Return the first matching address in content.
       *
       * @param content The string to search.
       * @return The first valid address, or null if no address was matched.
       */
      static String findAddress(String content) {
          Matcher houseNumberMatcher = sHouseNumberRe.matcher(content);
          int start = 0;
          while (houseNumberMatcher.find(start)) {
              if (checkHouseNumber(houseNumberMatcher.group(0))) {
                  start = houseNumberMatcher.start();
                  int end = attemptMatch(content, houseNumberMatcher);
                  if (end > 0) {
                      return content.substring(start, end);
                  }
                  start = -end;
              } else {
                  start = houseNumberMatcher.end();
              }
          }
          return null;
      }
  }
  