18

Here's my code, but I can't ever trigger the alert.

$(document).ready( function (){
    $("[id*='txtAddress1S']").blur(function() {
        var pattern = new RegExp('\b[P|p]*(OST|ost)*\.*\s*[O|o|0]*(ffice|FFICE)*\.*\s*[B|b][O|o|0][X|x]\b');
        if ($("[id*='txtAddress1S']").val().match(pattern)) {
            alert('We are unable to ship to a Post Office Box.\nPlease provide a different shipping address.'); 
        }

    });
});
TylerH
  • 20,799
  • 66
  • 75
  • 101
s15199d
  • 7,261
  • 11
  • 43
  • 70

16 Answers16

47

I tried several PO Box RegExp patterns found on the internet including the ones posted on Stack Overflow, none of them passed our test requirements. Hence, I posted our RegExp below and our test sets:

var poBox = /^ *((#\d+)|((box|bin)[-. \/\\]?\d+)|(.*p[ \.]? ?(o|0)[-. \/\\]? *-?((box|bin)|b|(#|n|num|number)?\d+))|(p(ost|ostal)? *(o(ff(ice)?)?)? *((box|bin)|b)? *(#|n|num|number)*\d+)|(p *-?\/?(o)? *-?box)|post office box|((box|bin)|b) *(#|n|num|number)? *\d+|(#|n|num|number) *\d+)/i;

var matches = [ //"box" can be substituted for "bin" 
    "#123", 
    "Box 123", 
    "Box-122", 
    "Box122", 
    "HC73 P.O. Box 217", 
    "P O Box125", 
    "P. O. Box", 
    "P.O 123", 
    "P.O. Box 123", 
    "P.O. Box", 
    "P.O.B 123",
    "P.O.B. 123", 
    "P.O.B.", 
    "P0 Box", 
    "PO 123", 
    "PO Box N", 
    "PO Box", 
    "PO-Box", 
    "POB 123", 
    "POB", 
    "POBOX123",
    "Po Box", 
    "Post 123", 
    "Post Box 123", 
    "Post Office Box 123", 
    "Post Office Box", 
    "box #123", 
    "box 122", 
    "box 123", 
    "number 123", 
    "p box", 
    "p-o box", 
    "p-o-box", 
    "p.o box", 
    "p.o. box", 
    "p.o.-box", 
    "p.o.b. #123", 
    "p.o.b.", 
    "p/o box", 
    "po #123", 
    "po box 123", 
    "po box", 
    "po num123", 
    "po-box", 
    "pobox", 
    "pobox123", 
    "post office box", 
    "Post Box #123" 
];

var nonMatches = [ 
    "The Postal Road", 
    "Box Hill", 
    "123 Some Street", 
    "Controller's Office", 
    "pollo St.", 
    "123 box canyon rd", 
    "777 Post Oak Blvd", 
    "PSC 477 Box 396", 
    "RR 1 Box 1020" 
]; 
sm83
  • 15
  • 6
Dathan
  • 1,125
  • 1
  • 9
  • 11
  • 777 Post Offie Box = returns false – kadalamittai Dec 11 '15 at 16:44
  • this seems to miss `Post Box #123` – anson Apr 14 '16 at 17:29
  • 2
    I think this one makes sense except for the "#123" being a valid po box – Cole W Sep 19 '16 at 18:55
  • This one doesn't match "123 P Street", "34 PO Road", or "777 Post Office Box Road", which I think is correct. This answer FTW! – Wesley Musgrove Oct 20 '17 at 13:09
  • 2
    I removed pound sign (like the first test #123) because that could be a valid building number or something: ^ *(((box|bin)[-. \/\\]?\d+)|(.*p[ \.]? ?(o|0)[-. \/\\]? *-?((box|bin)|b|(#|num)?\d+))|(p(ost)? *(o(ff(ice)?)?)? *((box|bin)|b)? *\d+)|(p *-?\/?(o)? *-?box)|post office box|((box|bin)|b) *(number|num|#)? *\d*\d+) – Tony Smith Aug 02 '18 at 19:56
  • This also matches "Expo Boulevard". – JonathanReez Sep 12 '18 at 01:31
  • 3
    Realize this old but it still ranks high in Google. The above regex fails on a number of valid addresses, just FYI for anyone finding this. Examples: 123 Harpo Box Street, 123 Poblano Lane – rg88 May 24 '19 at 15:57
  • If you want to include "postal" then change `p(ost)?` to `p(ost|ostal)?` – Ryan Walker Jun 03 '20 at 22:13
22

In javascript, you have to escape your slashes:

var pattern = new RegExp('\\b[P|p]*(OST|ost)*\\.*\\s*[O|o|0]*(ffice|FFICE)*\\.*\\s*[B|b][O|o|0][X|x]\\b');

Also, you could reduce your pattern a bit by using case-insensitive matching:

var pattern = new RegExp('\\b[p]*(ost)*\\.*\\s*[o|0]*(ffice)*\\.*\\s*b[o|0]x\\b', 'i');

Note: Your regex also matches on addresses such as:

  • 123 Poor Box Road
  • 123 Harpo Box Street

I would suggest also checking for a number in the string. Perhaps this pattern from a previous answer would be of some help:

var pattern = new RegExp('[PO.]*\\s?B(ox)?.*\\d+', 'i');

(it won't match on "Post Office" spelled out, or the numeric replacements.. but it's a start.)

Community
  • 1
  • 1
drudge
  • 35,471
  • 7
  • 34
  • 45
6

With Javascript its easier to use a regex literal like so:

var pattern = /\b(?:p\.?\s*o\.?|post\s+office)\s+box\b/i;

(No backslashes required!)

ridgerunner
  • 33,777
  • 5
  • 57
  • 69
3

This is what i have used accounting for spaces and case insensitive:

http://regexr.com/3cc2q

var pattern = /\bP(ost|ostal)?([ \.]*(O|0)(ffice)?)?([ \.]*Box)\b/i;
  • fail - po box
  • fail - p.o. box
  • fail - po bo
  • fail - post office box
  • fail - P.O. Box
  • fail - PO. Box
  • fail - PO Box
  • fail - POBox
  • fail - P O Box
  • fail - P.O Box
  • fail - PO
  • fail - Postal Box
  • fail - Post Office Box
  • pass - poop box
  • pass - pony box
Jonathan Marzullo
  • 6,879
  • 2
  • 40
  • 46
2

We ran into false positive PO Boxes after using @Dathan's answer in production for a few months. This simplified version ensures the pattern is followed by a number so won't match things like "Expo Blvd". It also allows things like "#123" and "B1" commonly found in address2 fields for apartment/unit/suite numbers. You can play around with it and add your own test cases here: https://regex101.com/r/7ZUQFl/2

const re = /^\s*(.*((p|post)[-.\s]*(o|off|office)[-.\s]*(b|box|bin)[-.\s]*)|.*((p|post)[-.\s]*(o|off|office)[-.\s]*)|.*((p|post)[-.\s]*(b|box|bin)[-.\s]*)|(box|bin)[-.\s]*)(#|n|num|number)?\s*\d+/i;
const matches = [
  "post office box 1",
  "post office b 1",
  "post off box 1",
  "post off b 1",
  "post o box 1",
  "post o b 1",
  "p office box 1",
  "p office b 1",
  "p off box 1",
  "p off b 1",
  "p o box 1",
  "p-o-b-1",
  "p.o.b.1",
  "POB1",
  "pob #1",
  "pob num1",
  "pob number1",
  "foo pob1",
  "box #1",
  "po 1",
  "pb 1"
];
const nonMatches = [ 
  "Expo Blvd",
  "Rural Route Box 1",
  "Army Post 1",
  "B1",
  "#1",
  "N1",
  "Number 1",
  "Num 1"
];
rocky
  • 1,037
  • 1
  • 11
  • 19
2

I found a pattern that works for most realistic post office addresses. Also had to fall to this after getting false positive with @jonathan-marzullo's answer for Rural Route Post 1 which is not a postal address.

My pattern is

pattern = P([.]?(O|0)[.]?|ost|ostal).((O|0)ffice.)?Box{1}\b/i

Matches for the following cases:

PO Box
P.O. Box
PO. Box
po box
P.o box
po box
p.o. box
post office box
P.O. Box
PO. Box
PO Box
Postal Box
Post Office Box
P.O Box
post office box 1
postal office box 1
postal box 1

Doesn't match the following cases:

post office b 1
post off box 1
post off b 1
post o box 1
post o b 1
p office box 1
p office b 1
p off box 1
p off b 1
p o box 1
p-o-b-1
p.o.b.1
POB
pob #1
pob num1
pob number1
foo pob1
box #1
po 1
pb 1
Expo Blvd
Rural Route Box 1
Army Post 1
postal office 1
poop box
pony box
POBox
P O Box
PO
po bo
Pobox
Post
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Jatto_abdul
  • 109
  • 2
  • 11
2

This regex works for me in all the scenarios. The main difference between

^([ A-Za-z0-9_:/#,]*)((((((P(ost(al)?)?)|(Mail(ing)?)))([ \\./#-]*)((((O|0)(ffice)?)|(B(ox|in)?))))|(B(ox|in)?))([ \\./#-]*))(B(ox|in)?)?(([ \\./#-]*)((n(um(ber)?)?)|no)?)([ \\.:/#-]*)([0-9]+)([ A-Za-z0-9_:/#,]*)$

This regular expression matches the following set of address patterns.

I also added ([ A-Za-z0-9_:/#,]*) pattern in the beginning as well as at the end to support the presence of the patterns in between any other address string. Such as 123 Main St, P.O.Box 458.

The construct is defined as follows: (((Postal|Mailing){SEP}(Office|(Box|Bin)))|(Box|Bin)){SEP}(Box|Bin)?{SEP}(number|no){SEP}([0-9]+)

It addresses the concerns mentioned above by anson, drudge, rocky, jatto abdul

This is validated using Java 8+

Pattern pattern = Pattern.compile(REG_EX, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE );

boolean result = pattern.matcher(sequence).matches();

Passed

  • P.O. Box: true
  • p.O. Box: true
  • p.o. Box: true
  • PO Box: true
  • Post Office Box: true
  • Post Box: true
  • po box: true
  • post b: true
  • po b: true
  • Post: true
  • Postal Office: true
  • Post Office Box: true
  • Postal Office Box: true
  • Mailing Office Box: true
  • Mail box: true
  • Box 123: true
  • Box-122: true
  • Box122: true
  • HC73 P.O. Box 217: true
  • P O Box125: true
  • P. O. Box: true
  • P.O 123: true
  • P.O. Box 123: true
  • P.O. Box: true
  • P.O.B 123: true
  • P.O.B. 123: true
  • P.O.B.: true
  • P0 Box: true
  • PO 123: true
  • PO Box: true
  • PO-Box: true
  • POB 123: true
  • POB: true
  • POBOX123: true
  • Po Box: true
  • Post 123: true
  • Post Box 123: true
  • Post Office Box 123: true
  • Post Office Box: true
  • box #123: true
  • box 122: true
  • box 123: true
  • P box: true
  • P-o box: true
  • P-o-box: true
  • P.o box: true
  • P.o. box: true
  • P.o.-box: true
  • P.o.b. #123: true
  • P.o.b.: true
  • P/o box: true
  • Po #123: true
  • Po box 123: true
  • Po box: true
  • Po num123: true
  • Po-box: true
  • Pobox: true
  • Pobox123: true
  • Post office box: true
  • Post Box #123: true

Failed

  • PO Box N: false
  • number 123: false
  • #123: false ( mentioned by Cole W. I removed it based on the similar logic mentioned by Tony Smith )
  • 123 Main St: false
  • 123 Poor Box St: false
  • 123 Poor Box Road: false
  • 123 Bin St: false
  • 123 Harpo Box Street: false
  • 123 Poblano Lane: false
  • The Postal Road: false
  • Box Hill: false
  • 123 Some Street: false
  • Controller's Office: false
  • pollo St.: false
  • 123 box canyon rd: false
  • 777 Post Oak Blvd: false
  • While this code may solve the question, [including an explanation](//meta.stackexchange.com/q/114762) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please [edit] your answer to add explanations and give an indication of what limitations and assumptions apply. – Yunnosch Feb 22 '22 at 18:02
  • I couldn't get 'Post 123' to pass using this, so I made a very small addition: a `?` after the first group with box/bin/etc. `/^([ A-Za-z0-9_:/#,]*)((((((p(ost(al)?)?)|(mail(ing)?)))([ \\./#-]*)((((o|0)(ffice)?)|(b(ox|in)?)))?)|(b(ox|in)?))([ \\./#-]*))(b(ox|in)?)?(([ \\./#-]*)((n(um(ber)?)?)|no)?)([ \\.:/#-]*)([0-9]+)([ A-Za-z0-9_:/#,]*)$/im` for Javascript. – Abe May 19 '22 at 23:38
1

This one is working pretty well for us. (php preg_match)

$pattern = '!p(ost)?\.?\s*o(ffice)?\.?(box|\s|$)!i';
mohrt
  • 464
  • 5
  • 3
1

If you stripped out all the dots "." and spaces then converted it lower case you would only need to check if the string starts with pob or postofficebox,

(credit BarCotter)

Abe
  • 190
  • 1
  • 9
0

Its worked for me,

var poRegex = /\bP(ost|ostal)?([ \.]*O(ffice)?)?([ \.]*Box)?\b/i;
0

Here's one that matches 'POB', 'PO Box'and 'Post Office Box'. Pattern:

\\b[PO.|Post\\sOffice]*\\s?B(ox)?.*\\d+\\b.

It's a modification of @drudge's solution.

baeyun
  • 228
  • 1
  • 6
0

The regex provided above is accepting PO box which is correct. Modified Regex not to accept is:

var pattern = /^ *(?!(#\d+)|((box|bin)[-. \/\\]?\d+)|(.*p[ \.]? ?(o|0)[-. \/\\]? *-?((box|bin)|b|(#|num)?\d+))|(p(ost)? *(o(ff(ice)?)?)? *((box|bin)|b)? *\d+)|(p *-?\/?(o)? *-?box)|post office box|((box|bin)|b) *(number|num|#)? *\d+|(num|number|#)|(?![a-zA-Z0-9\x20'#,]) *\d+)/i;
mcvnpvsr
  • 161
  • 1
  • 6
0

JavaScript:

^\b[P|p]*(OST|ost)*\.*\s*[O|o|0]*(ffice|FFICE)*\.*\s*[B|b][O|o|0][X|x]\b

You just need to start the regex with ^, like: ^\b[P|p]......

TylerH
  • 20,799
  • 66
  • 75
  • 101
  • Please take the time to point out what you changed in the Regex and why. For older questions with existing answers it is really useful to explain how your answer differs and if the passage of time since it was asked has made different answers available. – Jason Aller Sep 04 '19 at 17:12
0

Here is my version(in Java):

  1. It should have a number after PO BOX
  2. A valid PO BOX can be part of a longer address like "My address details PO BOX 123" is a valid PO BIN address.
  3. It can use BIN instead of BOX
import java.util.Random;
import java.util.regex.Pattern;

public class Demo {
    public static final Pattern POST_OFFICE_BOX_PATTERN = Pattern.compile("([\\w\\s*\\W]*(P(OST)?.?\\s*((O(FF(ICE)?)?)?.?\\s+B(IN|OX))+))\\s*\\d+[\\w\\s*\\W]*");

    public static void main(String[] args) {
        testInvalidAddresses();
        testValidAddresses();
    }

    public static void testValidAddresses() {
        String[] matches = new String[]{"HC73 P.O. Box 217",
                "P O Box125",
                "P.O. Box 123",
                "PO Box " + Math.abs(new Random().nextInt()),
                "PO Bin " + Math.abs(new Random().nextInt()),
                "Post Office Box 123",
                "Post Office Bin 123",
                "Post Off. Box 123",
                "Post Box 123",
                "po bin 123"};

        for (String address : matches) {
            boolean isPoBox = isValidPostOfficeBox(address.toUpperCase());
            if (!isPoBox) {
                throw new IllegalArgumentException(address);
            }
        }
    }

    public static void testInvalidAddresses() {
        var noMatches = new String[]{"#123",
                "Box 123",
                "Box-122",
                "Box122",
                "P. O. Box",
                "P.O. Box",
                "P.O.B 123",
                "P.O.B. 123",
                "P.O.B.",
                "P0 Box",
                "PO 123",
                "PO Box",
                "PO-Box",
                "POB 123",
                "POB",
                "POBOX123",
                "Po Box",
                "Post 123",
                "Post Office Box",
                "box #123",
                "box 122",
                "box 123",
                "p box",
                "p-o box",
                "p-o-box",
                "p.o box",
                "p.o. box",
                "p.o.-box",
                "p.o.b. #123",
                "p.o.b.",
                "p/o box",
                "po #123",
                "po bin",
                "po box",
                "po num123",
                "po-box",
                "pobox",
                "pobox123",
                "post office box",
                "The Postal Road",
                "Box Hill",
                "123 Some Street",
                "Controller's Office",
                "pollo St.",
                "123 box canyon rd",
                "777 Post Oak Blvd",
                "PSC 477 Box 396",
                "RR 1 Box 1020",
                "PzBzzzzzzzzz",
                "PzzBzzzzzzzzz",
                "PzzzBzzzzzzzzz",
                "PzzzzBzzzzzzzzz",
                "zzzPzBzzzzzzzzz",
                "zzzPzzBzzzzzzzzz",
                "zzzPzzzBzzzzzzzzz",
                "zzzPzzzzBzzzzzzzzz",
                "P.O 123",
                "Washington Post 42",
                "PO binary 123",
                "p b",
                "p  b",
                "Piebert",
                "Pebble",
                "pb"};

        for (String address : noMatches) {
            boolean isPoBox = isValidPostOfficeBox(address);
            if (isPoBox) {
                throw new IllegalArgumentException(address);
            }
        }
    }

    public static boolean isValidPostOfficeBox(String value) {
        if (value != null) {
            return POST_OFFICE_BOX_PATTERN.matcher(value.toUpperCase()).matches();
        }
        return false;
    }
}
valijon
  • 1,304
  • 2
  • 20
  • 35
0

I used following JavaScript regex for my project requirements.

/((((p[\s\.]?)\s?[o\s][\.]?)\s?)|(post\s?office\s?)|(postal\s?)|(post\s?))((box|bin|b\.?)?\s?((num|number|no|#)\.?)?\s?\d+)/igm.test(v);

Following are not allowed.

po 123
pob 555
p.o.b. 555
po box 555
pobox 555
p.o. box 663
P.O. Box #123
P.O. Box 3456
PO Box 1234
PO Box Num 1234
P O Box 4321
Post Office Box 9999
postal box 123
postal box #123
postal b 123
postal bin #45
postal bin 35
postal bin number 678
postal bin no 980
down street PO box 123
po bin 123
po bin #45
po number 45
p.o.#54
p.o #54
P.o.B #45
POB 54
POB# 454
POb #65464
My apartment floor 23 po box 34
Down town, near supermarket postal box 45
bin postal 123

Following are allowed

95 CAPON ST NE, PALM BAY, FL, 32905
713 BIGGIN POND RD
123 Poor Box Road
123 Harpo Box Street
The Postal Road
#43 The postal Road
Box Hill
123 Some Street
Controller's Office
pollo St.
123 box canyon rd
777 Post Oak Blvd
PSC 477 Box 396
RR 1 Box 1020
demo village 123
House #4, near lampost
3rd lane Radio communication Pole
Second house from Polly store
4th house from postal office
Fox cinema post #123
Mukesh
  • 7,630
  • 21
  • 105
  • 159
0

None of the answers here worked exactly how I needed, so here's my solution along with some useful code.

Here's the link to Regex101: https://regex101.com/r/17WThE

Note: I recommend generating the regex from the code down further, but if you just want the regex itself, here it is:

const poBoxRegex = /\b(((?:(P(?:ost(?:al)?)?)[ ./\-_]*(O(?:ff(?:ice)?)?)?[ ./\-_]*(b(?:o?x|in)?))|(?:(P(?:ost(?:al)?)?)[ ./\-_]*(O(?:ff(?:ice)?)?)[ ./\-_]*(b(?:o?x|in)?)?)|(?:(?<!(r(?:ural)?[ ./\-_]*r(?:oute)?)[ ./\-_]*((n\.?(?:o|um(?:ber)?)?\.?)?[ ./\-_]*#*[ ./\-_]*(\d+))?[ ./\-_]*)(box|bin)))[ ./\-_]*((n\.?(?:o|um(?:ber)?)?\.?)?[ ./\-_]*#*[ ./\-_]*(\d+)))\b/i;

Notes and features:

  • Case insensitive (of course), and respects word boundaries.
  • All words can all be separated by nothing, spaces, periods, slashes, dashes, or underscores. (So "PO Box 123", "P.O. Box 123", "PO-Box 123", "PO_Box123", etc. all match.)
  • Some valid abbreviations are P/Post/Postal, O/Off/Office, and B/Box/Bx/Bin (as well as N/No/Num/Number* with "#" optionally). The only exception is when using only "box" or "bin", you must use the full word without abbreviation (so "box" or "bin", not "b" or "bx").
  • Valid combinations (not exact words) are Post Office Box, Post Box, Post Office, or just Box (all must have numbers). So it doesn't match things like "Post 123", "Postal 123", "Office 123", or "Mailbox 123". Currently it doesn't match with "mailbox", and there has to be more than just "post", "postal", or "office" in the address.
  • Anything with a Box + number will match, with the exception of rural route addresses (like "RR 1 Box 2", "R.R. 23 Box 45", or "Rural Route Box 123"). So it also matches military APO/FPO address, like "PSC 123 Box 1234" (because of the Box + number).
  • The PO Box must have a number after it in order to match. This helps prevent false positives. The number regex is very fuzzy, so it will match a lot of things, like "1", "#1", "No. # 1", "number 1", "Num 1", etc.
  • One area for improvement could be adding matches for the words in other languages, such as "Postfach" (German).

Generator code and util functions: (you should probably put this into its own file/module)

// Get the string version of each regex that we need to match PO boxes
const separator = /[ ./\-_]/.source; // Any of these characters can separate the PO box words (space, period, slash, dash, underscore)
const postalGroup = /(P(?:ost(?:al)?)?)/i.source; // "P", "Post", "Postal"
const officeGroup = /(O(?:ff(?:ice)?)?)/i.source; // "O", "Off", "Office"
const boxGroup = /(b(?:o?x|in)?)/i.source; // "b", "bx", "box", "bin"
const fullBoxGroup = /(box|bin)/i.source; // "box" or "bin" (full word only)
const ruralRouteGroup = /(r(?:ural)? r(?:oute)?)/i.source; // "rr", "r r", "ruralr", "rroute", "rural route", etc. (space will be replaced with full separators later)
const numTextGroup = /(n\.?(?:o|um(?:ber)?)?\.?)/i.source; // "n", "no", "num", "number", "n.", "n.o.", "no.", "num.", etc.
const digitsGroup = /(\d+)/.source; // 1 or more digits

// Spaces in below strings will be replaced later with the separator regex

// Construct number part of PO box regex string
const poBoxNumberGroup = `(${numTextGroup}? #* ${digitsGroup})`; // Match "number # 123", "no 123", "# 123", "123", etc.
// Construct PO box words regex string
const poBoxWordsOfficeOptional = `(?:${postalGroup} ${officeGroup}? ${boxGroup})`; // Match stuff like "post box", "post office box", or "P B" (where the office part is optional)
const poBoxWordsBoxOptional = `(?:${postalGroup} ${officeGroup} ${boxGroup}?)`; // Match stuff like "post office", "post office box", or "PO" (where the box part is optional)const ruralRouteNegativeGroup = /(?!r(?:ural)? r(?:oute)? (\d+)?)/i.source; // Negative lookahead for "rr 12", "r r 1", "ruralr 1", "rroute 1", "rural route 1", etc. (space will be replaced with full separators later)
const poBoxWordsBoxOnly = `(?:(?<!${ruralRouteGroup} ${poBoxNumberGroup}? )${fullBoxGroup})`; // Match just "box" or "bin" (unless preceded by a rural route)
const poBoxWordsGroup = `(${poBoxWordsOfficeOptional}|${poBoxWordsBoxOptional}|${poBoxWordsBoxOnly})`; // Match either of the above
// Construct the whole PO box regex string (with word boundaries, but still excluding the separators)
const wholePOBoxGroup = `\\b(${poBoxWordsGroup} ${poBoxNumberGroup})\\b`;

// Construct the final PO box regex
const PO_BOX_REGEX = new RegExp(
  wholePOBoxGroup.replaceAll(" ", `${separator}*`), // Replace all spaces with regex matching any number of the separators
  "i" // Making global causes issues with matching since the regex is a constant
);

// Check if the address is a PO box
export function hasPOBox(addressString) {
  return PO_BOX_REGEX.test(addressString);
}

// Get the PO box part from the address (first match)
export function getPOBox(addressString) {
  const match = addressString.match(PO_BOX_REGEX);
  return match ? match[1] : null;
}

// Get the PO box part from the address (all matches)
export function getAllPOBoxes(addressString) {
  const poBoxRegex = new RegExp(PO_BOX_REGEX, "gi"); // Make global so we can get all matches
  const matches = addressString.matchAll(poBoxRegex);
  return Array.from(matches, (match) => match[1]);
}

// Given a string containing PO boxes, standardize all PO boxes found in the string like "PO Box 123"
export function standardizePOBoxes(text) {
  const poBoxes = getAllPOBoxes(text || "");
  for (const poBox of poBoxes) {
    const poBoxNum = poBox.match(/\d+/)?.[0]; // Get the number part of the PO box
    text = text?.replace(poBox, "PO Box " + poBoxNum); // Replace the PO box with the formatted version
  }
  return text;
}

The regex will be the PO_BOX_REGEX constant. Generating the regex in this way allows you to more easily see how it works and make appropriate modifications.

Finally, below is a list of matches and non-matches I'm using in my tests:

const matches = [
  "BIN 12",
  "BOX 12",
  "Box 12",
  "Box-12",
  "Box12",
  "P O Box123",
  "P. O. Box 13",
  "P.O 123",
  "P.O Box 12",
  "P.O. Box 123",
  "P.O.  Box  123",
  "P.O.B 123",
  "P.O.B. 123",
  "P.o box 12",
  "PO 123",
  "PO Box 1",
  "PO Box 123",
  "PO Box N 12",
  "PO Box No 12",
  "PO Box No. 12",
  "PO Box Number 12",
  "PO Box #12",
  "PO Box # 12",
  "PO-Box 12",
  "PO. Box 12",
  "POB 12",
  "POB 123",
  "POB1",
  "POBOX123",
  "Po Box 12",
  "Post Box #123",
  "Post Box 123",
  "Post Office Box 123",
  "Postal Box 12",
  "box #1",
  "box #123",
  "box # 123",
  "box 123",
  "p box 12",
  "p o box 12",
  "p o box num 12",
  "p off b 12",
  "p off box 12",
  "p office b 12",
  "p office box 12",
  "p-o box 12",
  "p-o-b-1",
  "p-o-box-12",
  "p.o.bin 12",
  "p.o box 12",
  "p.o. box 12",
  "p.o. box. 12",
  "p.o.-box12",
  "p.o.b.#123",
  "p.o.b.12",
  "p/o box 123",
  "p/o-box 12",
  "pb 12",
  "po #123",
  "po 12",
  "po bin 123",
  "po box 123",
  "po box no #23",
  "po box no 123",
  "po box n.o. 12",
  "po box num 12",
  "po box num #123",
  "po box number #12",
  "po box number 12",
  "po bx 12",
  "po n 12",
  "po num123",
  "po-box-12",
  "pob #12",
  "pob num12",
  "pob number12",
  "pobox123",
  "post o. b. 12",
  "post o box 123",
  "post o bx 12",
  "post off b 12",
  "post off. box 12",
  "post office b 12",
  "post office box 123",
  "postal box 123",
  "postal office box 12",
  "postal-off-box 12",
];

const nonMatches = [
  // Don't match unit numbers
  "B1",
  "#1",
  "# 1",
  "N1",
  "Number 1",
  "Num 1",
  "No 1",
  // Rural route addresses
  "RR 12 Box 1020",
  "RR Box 12",
  "RR #12 Box 12",
  "r.r. 12 box 12",
  "Rural Route 12 Box 12",
  "Rural Route # 12 Box 12",
  "Rural Route Box 12",
  "rural-route 12 box 12",
  // Other street addresses
  "1223 P Street #1",
  "123 ABox #1", // Respect word boundary
  "123 Box, #1", // This would match, but the comma prevents it
  "123 Bx #1", // Must use the full word "box" when by itself
  "123 Expo #1",
  "123 Harpo Box Street #1",
  "123 Poblano Lane #1",
  "123 Poor Box Road #1",
  "123 Some Street",
  "123 box canyon rd",
  "2 Expo Blvd #1",
  "34 PO Road #1",
  "777 Post Oak Blvd",
  "Army Post 1",
  "Box Hill",
  "Controller's Office",
  "Office 123",
  "Post 123", // Perhaps this should match?
  "Postal 123", // Perhaps this should match?
  "Post Office Road 123", // "Road" before the number prevents this from matching
  "Post Rd. #1",
  "Postal Road #1",
  "The Postal Road",
  "pollo St.",
];

This isn't necessary a complete list, and may be redundant in some ways. I copied a lot of these from the comments on this question's replies, so hopefully it covers a wide range of cases.

I hope someone finds this useful, and let me know if you have any suggestions or find any problems with it.