diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000000000000000000000000000000000000..deef8f450dc35f255bfaa7a3ae8e4ce9002ae9e3
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,32 @@
+2020-07-21  David Byers  <david.byers@liu.se>
+
+	Wrote texinfo documentation:
+	* olc.el (olc-parse-length): Removed.
+	Updated documentation comment.
+	(olc-decode): Updated docstring.
+	(olc-encode): Updated docstring.
+	(olc-recover): Updated docstring.
+	(olc-shorten): Updated docstring.
+	(olc-recover-string): Updated docstring.
+
+	* olc.texi: New file.
+
+	Implement shorten:
+	* test/olctest.el (olctest-decode): Change olc-code-length to
+	olc-code-precision.
+	(olctest-shortcodes): Implement shorten tests.
+	(olctest-localtests): New function.
+
+	* olc.el
+	(olc-code-precision): New function.
+	(olc-latitude-precision): Change ffloor to floor.
+	(olc-shorten-error): New error.
+	(olc-clip-latitude): New function.
+	(olc-normalize-latitude): Use it. Convert to subst. Renamed length
+	to len to reduce confusion.
+	(olc-normalize-longitude): Convert to subst.
+	(olc-valid-char): Reimplemented missing function.
+	(olc-shorten): New function.
+	(olc-recover): Use olc-clip-latitude, not normalize.
+
+
diff --git a/README.md b/README.md
index 13316d5cf1a18010c757c5dd263c0ef0a2571389..9868235787465b5969f5c5a33c7fd4bff0c9ca1c 100644
--- a/README.md
+++ b/README.md
@@ -20,98 +20,50 @@ implements all the required and most of the optional features in the
 standard, and passes the test cases published in the open location
 code github repository (see above).
 
-### Data structures
+The complete documentation is available in texinfo format. The
+following examples may be helpful.
 
-#### OLC Area
+## Examples
 
-An OLC is the area represented by an open location code. All fields
-are read-only once the object has been created.
+### Encoding
 
-`(olc-area-create :latlo LATLO :lonlo LONLO :lathi LATHI :lonhi LONHI)`
-: Creates an OLC area with southwest corner `LATLO`,`LONLO` and
-  northeast corner `LATHI`,`LONHI`.
+````
+(olc-encode 58.397813 15.576063 11)
+"9FCQ9HXG+4CG"
+````
 
-`(olc-area-p OBJ)`
-: Return non-nil if `OBJ` is an OLC area.
+### Decoding
 
-`(olc-area-latlo AREA)`
-`(olc-area-lonlo AREA)`
-`(olc-area-lathi AREA)`
-`(olc-area-lonhi AREA)`
-: Get the south, west, north, and east coordinates of the area,
-  respectively.
+````
+(olc-decode "9FCQ9HXG+4CG")
+#s(olc-area 58.397800000000004 15.5760625 58.397825000000005 15.57609375)
+(olc-area-lat (olc-decode "9FCQ9HXG+4CG"))
+58.3978125
+(olc-area-lon (olc-decode "9FCQ9HXG+4CG"))
+````
 
-`(olc-area-lat AREA)`
-`(olc-area-lon AREA)`
-: Get the center latitude and longitude of the area, respectively.
+### Shortening
 
-#### OLC Parse
+````
+(olc-shorten "9C3W9QCJ+2VX" 51.3701125 -1.217765625)
+"+2VX"
+(olc-shorten "9C3W9QCJ+2VX" 51.3701125 -1.217765625 4)
+"9QCJ+2VX"
+````
 
-The OLC parse is a structure mainly used internally. Unless you call
-`olc-parse-code` you will probably never see one.
+### Recovery
 
-`(olc-parse-create &keys pairs grid short prec code)`
-: Creates an OLC parse structure. Don't call this: use
-  `olc-parse-code` instead.
+Recovery using latitude and longitude as reference:
 
-`(olc-parse-pairs PARSE)`
-: Returns the list of parsed pairs from the code (pairs are before the
-  plus sign and the first two characters after, if present).
+````
+(olc-recover "+2VX" 51.3701125 -1.217765625)
+"9C3W9QCJ+2VX"
+````
 
-`(olc-parse-grid PARSE)`
-: Returns the list of parsed grid digits from the code (the optional
-  digits that follow the last pair).
+Recovery using a geographical reference (requires `requests` and uses
+the OpenStreetMap API):
 
-`(olc-parse-short PARSE)`
-: Non-nil if the parsed code was shortened.
-
-`(olc-parse-precision PARSE)`
-: Precision of the parsed code. Padded codes can have precisions lower
-  than 8. All other full and all short codes have precision of at
-  least 8 (although, don't cound on short codes always having
-  precision 8 or more).
-
-`(olc-parse-code PARSE)`
-: The parsed code.
-
-
-### Functions
-
-`(olc-encode lat lon len)`
-: Encode a latitude LAT, longitude LON, into an open location code of
-  length LEN. All arguments will be clipped to acceptable values.
-
-`(olc-decode code)`
-: Decode a code CODE. Returns an OLC area (see above).
-
-`(olc-recover code lat lon &optional format)`
-: Recover the closest point to coordinates `LAT` and `LON` with a code
-  that can be shortened to `CODE`. If FORMAT is `'latlon`, then the
-  center of the recovered area `(LAT . LON)` is returned. If FORMAT is
-  `'area` (or any other value), the returned value is an full open
-  location code.
-
-`(olc-recover-string arg1 &optional arg2 arg3)`
-: Recover a shortened code *without* the reference latitude and
-  longitude. When called with one argument, it must be a string
-  consisting of a shortened open location code followed by whitespace
-  and a geographical location. When called with two strings, the first
-  must be a shortened open location code and the second if the
-  geographical location. Optionally, the last argument in either case
-  can be a symbol indicating the format of the return value (see
-  `olc-recover`, above). This function requires the `request` package
-  to be installed, and uses the Open Streetmap API to convert the
-  geographical reference to coordinates. Please make sure you follow
-  the acceptable use policy for the API (e.g., one request per second,
-  tops, allowed).
-
-`(olc-is-valid CODE)`
-: Returns non-nil if `CODE` is a valid open location code.
-
-`(olc-is-short CODE)`
-: Returns non-nil if `CODE` is a valid short location code. Returns
-  nil for valid short and for invalid codes.
-
-`(olc-is-full CODE)`
-: Returns non-nil if `CODE` is a valid full open location code.
-  Returns nil for valid long and for invalid codes.
+````
+(olc-recover-string "M24Q+89 Mutitjulu")
+"5Q6HM24Q+89"
+````
diff --git a/olc.el b/olc.el
index be38e8ebd7cdd7dbe99f3065060e3a31f7c2923d..64715dca9d12148e4fe9b8af516f1ca09be55165 100644
--- a/olc.el
+++ b/olc.el
@@ -19,31 +19,20 @@
 ;;; ========================================================================
 ;;; This program provides basic open location code support in emacs
 ;;; lisp. The support for recovering shortened codes depends on the
-;;; request library and uses Open Streetmap; please check the terms of
+;;; request library and uses OpenStreetMap; please check the terms of
 ;;; use for the service to ensure that you remain compliant.
 ;;;
 ;;; All methods required by the open location code specification are
 ;;; provided in some form. The implementation passed the tests present
 ;;; in the open location code github repository at the time of writing
 ;;; almost cleanly -- there are some minor rounding issues in decode.
-;;;
-;;; olc-encode encodes latitude and longitude to any length code.
-;;; olc-decode decodes any length code (without reference location).
-;;; olc-recover recovers shortened codes
-;;;
-;;; olc-is-valid checks for valid codes (long or short).
-;;; olc-is-short checks for valid short codes.
-;;; olc-is-full checks for valid full codes.
-;;; olc-valid-digits checks for valid digits.
-;;;
-;;; There is no support for shortening codes.
 ;;; ========================================================================
 
 
 ;; This is me being dragged kicking and screaming into the 21st
-;; century because the alternative is to include my own structured
-;; data code -- which would be overkill -- or do it manually -- which is
-;; a pain in the read end. So cl-lib it is.
+;; century because the alternative is to cl-lib is to include my own
+;; structured data code (which would be overkill) or do it manually
+;; (which is a pain in the backside). So cl-lib it is.
 
 (require 'cl-lib)
 (require 'request nil t)
@@ -57,6 +46,7 @@
 (define-error 'olc-parse-error "Parse error in open location code" 'olc-error)
 (define-error 'olc-decode-error "Error decoding open location code" 'olc-error)
 (define-error 'olc-encode-error "Error encoding open location code" 'olc-error)
+(define-error 'olc-shorten-error "Error shortening open location code" 'olc-error)
 
 ;; ========================================================================
 ;; Mapping of digits to base 20 values
@@ -93,11 +83,6 @@
   (short nil :read-only t)
   (precision nil :read-only t))
 
-(defsubst olc-parse-length (parse)
-  "Get length from a parsed open location code PARSE."
-  (+ (* 2 (length (olc-parse-pairs parse)))
-     (length (olc-parse-grid parse))))
-
 (cl-defstruct (olc-area (:copier nil)
                         (:constructor olc-area-create))
   (latlo nil :read-only t)
@@ -118,6 +103,10 @@
 ;; (Mostly) internal functions
 ;; ========================================================================
 
+(defmacro olc-valid-char (char)
+  "Check if CHAR is a valid OLC digit."
+  `(assq ,char olc-digit-mapping))
+
 (defmacro olc-transform-error (spec &rest body)
   "Catch some errors and throw others."
   (declare (indent 1))
@@ -125,15 +114,19 @@
        ,@body
      (,(elt spec 0) (signal ',(elt spec 1) (list ,@(cddr spec))))))
 
-(defun olc-normalize-latitude (lat length)
+(defsubst olc-clip-latitude (lat)
+  "Clip LAT to -90,90"
+  (max -90 (min 90 lat)))
+
+(defsubst olc-normalize-latitude (lat len)
   "Normalize latitude LAT."
-  (setq lat (max -90 (min 90 lat)))
+  (setq lat (olc-clip-latitude lat))
   (when (= lat 90.0)
-    (setq lat (- lat (/ (olc-latitude-precision length) 2.0))))
+    (setq lat (- lat (/ (olc-latitude-precision len) 2.0))))
   lat)
 
 
-(defun olc-normalize-longitude (lon)
+(defsubst olc-normalize-longitude (lon)
   "Normalize longitude LON."
   (while (< lon -180) (setq lon (+ lon 360)))
   (while (>= lon 180) (setq lon (- lon 360)))
@@ -142,7 +135,7 @@
 (defun olc-latitude-precision (len)
   "Compute latitude precision in code of length LEN."
   (if (<= len 10)
-      (expt 20 (- (ffloor (+ 2 (/ len 2)))))
+      (expt 20 (- (floor (+ 2 (/ len 2)))))
     (/ (expt 20 -3) (expt 5 (- len 10)))))
 
 (defun olc-parse-code (code)
@@ -254,12 +247,19 @@ invalid."
       (not (olc-parse-short (olc-parse-code code)))
     (olc-parse-error nil)))
 
+(defun olc-code-precision (code)
+  "Return the precision of CODE."
+  (condition-case nil
+      (olc-parse-precision (olc-parse-code code))
+    (olc-parse-error nil)))
+
 (defun olc-decode (code)
   "Decode open location code CODE.
 
-Returns a olc-parse structure or raises olc-parse-error if
-the code is invalid or olc-decode-error if it cannot (legally) be
-decoded.
+Returns an `olc-area' structure. Raises `olc-parse-error' if the
+code can't be parsed, and `olc-decode-error' if it can't be
+decoded (e.g. a padded shortened code, a padded code with grid
+coordinates, an empty code, and so forth).
 
 Since this function uses floating point calculations, the results
 are not identical to e.g. the C++ reference implementation. The
@@ -296,13 +296,17 @@ differences, however, are extremely small."
 (defun olc-encode (lat lon len)
   "Encode LAT and LON as a LEN length open location code.
 
+The length is automatically clipped to between 2 and
+15. `olc-encode-error' is raised if the length is otherwise
+invalid (i.e. 3, 5, 7, or 9).
+
 Returns an olc-area structure. Raises olc-encode-error if the
 values cannot (legally) be encoded to the selected length."
   (setq len (max 2 (min 15 len)))
   (when (and (< len 11) (/= (% len 2) 0))
     (signal 'olc-encode-error "invalid encoding length"))
 
-  (setq lat (olc-normalize-latitude lat length)
+  (setq lat (olc-normalize-latitude lat len)
         lon (olc-normalize-longitude lon))
 
   (let ((code nil)
@@ -343,13 +347,15 @@ values cannot (legally) be encoded to the selected length."
 (defun olc-recover (code lat lon &optional format)
   "Recover shortened code CODE from coordinates LAT and LON.
 
-Optional FORMAT specifies the result format. 'latlon means return
-the center latitude and longitude as a pair. 'area (the default)
-means return an olc-area."
+Recovers the closest point to coordinates LAT and LON with a code
+that can be shortened to CODE. If FORMAT is `latlon', then the
+center of the recovered area (LATITUDE . LONGITUDE) is returned.
+If FORMAT is `area' (or any other value), the returned value is an
+full open location code."
   (let ((parse (olc-parse-code code)))
     (if (olc-is-full parse)
         (upcase code)
-      (setq lat (olc-normalize-latitude lat length)
+      (setq lat (olc-clip-latitude lat)
             lon (olc-normalize-longitude lon))
       (let* ((padlen (- (olc-parse-precision parse)
                         (* 2 (length (olc-parse-pairs parse)))
@@ -373,18 +379,49 @@ means return an olc-area."
               (t (olc-encode lat lon (olc-parse-precision parse))))))))
 
 
+(defun olc-shorten (code lat lon &optional limit)
+  "Attempt to shorten CODE with reference LAT and LON.
+
+Shorten CODE, which must be a full open location code, using
+latitude LAT and longitude LON as the reference. If LIMIT is
+specified, then the code will be shortened by at most that many
+digits. If the code can't be shortened, the original code is
+returned. `olc-shorten-error' is raised if CODE is a padded or
+shortened code, of if LIMIT is not positive and even."
+  (let* ((parse (olc-parse-code code))
+         (area (olc-decode parse)))
+    (when (null limit) (setq limit 12))
+    (unless (and (> limit 0) (= 0 (% limit 2)))
+      (signal 'olc-shorten-error (list "limit must be even and positive" code)))
+    (when (olc-is-short parse)
+      (signal 'olc-shorten-error (list "can't shorten shortened codes" code)))
+    (when (< (olc-parse-precision parse) 8)
+      (signal 'olc-shorten-error (list "can't shorten padded codes" code)))
+
+    (setq lat (olc-clip-latitude lat)
+          lon (olc-normalize-longitude lon))
+
+    (let ((coderange (max (abs (- (olc-area-lat area) lat))
+                          (abs (- (olc-area-lon area) lon)))))
+      (catch 'break
+        (dolist (spec '((4 . 0.0025) (3 . 0.05) (2 . 1) (1 . 20)))
+          (when (< coderange (* (cdr spec) 0.3))
+            (throw 'break (substring code (min limit (* (car spec) 2))))))
+        code))))
+
 (defun olc-recover-string (string &optional reference format)
   "Recover a location from a shortened open location code and reference.
 
-When called with one string argument, the string is assumed to
-contain the code followed by whitespace, and then a reference
-location as text.
+When called with one argument, it must be a string consisting of a
+shortened open location code followed by whitespace and a geographical
+location.
 
-When called with two string arguments, the first is assumed to be
-the short code and the second is the reference location as text.
+When called with two strings, the first must be a shortened open
+location code and the second if the geographical location.
 
-A symbol may be included as the last argument to select the
-result format. See olc-recover for details."
+Optionally, the last argument in either case can be a symbol
+indicating the format of the return value (see `olc-recover' for
+details)."
   (unless (fboundp 'request)
     (error "request library is not loaded"))
   (let (code resp)
diff --git a/olc.texi b/olc.texi
new file mode 100644
index 0000000000000000000000000000000000000000..e8627d7b4d0eed83fe90a7d8056deaae923bc838
--- /dev/null
+++ b/olc.texi
@@ -0,0 +1,248 @@
+\input texinfo
+@documentencoding utf-8
+@setfilename olc
+@settitle Open Location Code for emacs
+
+@titlepage
+@title Open Location Code for emacs
+@author David Byers
+@end titlepage
+
+@node Top
+@unnumbered Introduction
+
+Open Location Code is a way to encode locations in a format that is
+easier for people (not computers) to use than latitude and longitude.
+
+For example, the code 9FCQ9HXG+4C refers to the location 58°23'52.1"N
+15°34'33.8"E (58.397813, 15.576063).
+
+Codes can be shortened by removing characters from the beginning
+andding a reference location: 9HXG+4C with the reference "Linköping"
+would refer to the same set of coordinates.
+
+For details about open location code and implementations in other
+languages, see https://github.com/google/open-location-code.
+
+@menu
+* Data types::  Data types defined by olc.
+* Functions::   Functions defined by olc.
+* Index::       Type and function index.
+@end menu
+
+@node Data types,Functions,,Top
+@unnumbered Data types
+
+olc defines two data types: olc-area and olc-parse. The former
+represents the result of decoding a code and the latter is the result
+of parsing a code, and is mostly for internal use.
+
+@menu
+* olc-area::      The olc-area data type.
+* olc-parse::     The olc-parse data type.
+@end menu
+
+@node olc-area,olc-parse,,Data types
+@unnumberedsec olc-area
+
+An olc-area is the area represented by an open location code. All fields
+are read-only once the object has been created.
+
+@defun olc-area-create &key latlo lonlo lathi lonhi
+Creates an olc-area with southwest corner (@var{latlo},@var{lonlo}) and
+northeast corner (@var{lathi},@var{lonhi}).
+@end defun
+
+@defun olc-area-p obj
+Return non-@code{nil} if @var{obj} is an olc-area.
+@end defun
+
+@defun olc-area-latlo area
+Return the southern latitude of @var{area}.
+@end defun
+
+@defun olc-area-lonlo area
+Return the eastern longitude of @var{area}.
+@end defun
+
+@defun olc-area-lathi area
+Return the northern latitude of @var{area}.
+@end defun
+
+@defun olc-area-lonhi area
+Return the western longitude of @var{area}.
+@end defun
+
+@defun olc-area-lat area
+Return the latitude of the center of @var{area}.
+@end defun
+
+@defun olc-area-lon area
+Return the longitude of the center of @var{area}.
+@end defun
+
+
+@node olc-parse,,olc-area,Data types
+@unnumberedsec olc-parse
+
+The olc-parse is a structure mainly used internally. Unless you call
+@code{olc-parse-code} you will probably never see one.
+
+@defun olc-parse-create &key pairs grid short prec code
+Create an olc-parse structure. Don't call this: use
+@code{olc-parse-code} instead.
+@end defun
+
+@defun olc-parse-pairs parse
+Returns the list of parsed pairs in @var{parse}. Pairs are first ten
+digits of a full code (five pairs). For padded and shortened codes,
+the list of pairs could be shorter.
+@end defun
+
+@defun olc-parse-grid parse
+Returns the list of parsed grid digits in @var{parse}. Grid digits are
+all (up to five) the digits that follow the last pair.
+@end defun
+
+@defun olc-parse-short parse
+Return non-@code{nil} if @var{parse} represents a shortened code.
+@end defun
+
+@defun olc-parse-precision parse
+Return the precision in digits of the parsed code in @var{parse}. A
+full code without padding will have precision 8, 10, or more. Full
+codes with padding have precision 6 or lower. Shortened codes should
+have at least a precision of 8 since padded codes can't be shortened,
+but don't count on this.
+@end defun
+
+
+@node Functions,,Data types,Top
+@unnumberedsec Functions
+
+@defun olc-encode lat lon len
+Encode a latitude @var{lat}, longitude @var{lon}, into an open
+location code of length @var{len}. The length is automatically clipped
+to between 2 and 15. (@code{olc-encode-error} is raised if the length
+is otherwise invalid (i.e. 3, 5, 7, or 9).
+
+@example
+@group
+(olc-encode 58.397813 15.576063 11)
+@result{} "9FCQ9HXG+4CG"
+(olc-encode 58.397813 15.576063 8)
+@result{} "9FCQ9HXG+"
+(olc-encode 58.397813 15.576063 4)
+@result{} "9FCQ0000+"
+@end group
+@end example
+@end defun
+
+@defun olc-decode code
+Decode @var{code} and return an @code{olc-area} representing the
+location. Raises @code{olc-parse-error} if the code can't be parsed,
+and @code{olc-decode-error} if it can't be decoded (e.g. a padded
+shortened code, a padded code with grid coordinates, an empty code,
+and so forth). Returns an olc-area structure.
+
+@example
+@group
+(olc-decode "9FCQ9HXG+4CG")
+@result{} #s(olc-area 58.397800000000004 15.5760625 58.397825000000005 15.57609375)
+(olc-area-lat (olc-decode "9FCQ9HXG+4CG"))
+@result{} 58.3978125
+(olc-area-lon (olc-decode "9FCQ9HXG+4CG"))
+@result{} 15.576078125
+@end group
+@end example
+@end defun
+
+@defun olc-shorten code lat lon &optional limit
+Shorten @var{code}, which must be a full open location code, using
+latitude @var{lat} and longitude @var{lon} as the reference. If
+@var{limit} is specified, then the code will be shortened by at most
+that many digits. If the code can't be shortened, the original code is
+returned. @code{olc-shorten-error} is raised if @var{code} is a padded
+or shortened code, of if @var{limit} is not even.
+
+@example
+@group
+(olc-shorten "9C3W9QCJ+2VX" 51.3701125 -1.217765625)
+@result{} "+2VX"
+(olc-shorten "9C3W9QCJ+2VX" 51.3701125 -1.217765625 4)
+@result{} "9QCJ+2VX"
+@end group
+@end example
+@end defun
+
+@defun olc-recover code lat lon &optional format
+Recover the closest point to coordinates @var{lat} and @var{lon} with
+a code that can be shortened to @var{code}. If @var{format} is
+@code{latlon}, then the center of the recovered area (@var{latitude} .
+@var{longitude}) is returned. If @var{format} is @code{area} (or any other
+value), the returned value is an full open location code.
+
+@example
+@group
+(olc-recover "+2VX" 51.3701125 -1.217765625)
+@result{} "9C3W9QCJ+2VX"
+(olc-recover "+2VX" 51.3701125 -1.217765625 'latlon)
+@result{} (51.370112500000005 . -1.2177656250000002)
+@end group
+@end example
+@end defun
+
+@defun olc-recover-string arg1 &optional arg2 arg3
+Recover a shortened code @i{without} the reference latitude and
+longitude.
+
+When called with one argument, it must be a string consisting of a
+shortened open location code followed by whitespace and a geographical
+location.
+
+When called with two strings, the first must be a shortened open
+location code and the second if the geographical location.
+
+Optionally, the last argument in either case can be a symbol
+indicating the format of the return value (see @code{olc-recover} for
+details).
+
+@example
+@group
+(olc-recover-string "M24Q+89 Mutitjulu")
+@result{} "5Q6HM24Q+89"
+(olc-recover-string "M24Q+89" "Mutitjulu")
+@result{} "5Q6HM24Q+89"
+(olc-recover-string "M24Q+89" "Mutitjulu" 'latlon)
+@result{} (-25.344187500000004 . 131.0384375)
+@end group
+@end example
+
+This function requires the @code{request} package to be installed, and
+uses the OpenStreetMap API to convert the geographical reference to
+coordinates. Please make sure you follow the acceptable use policy for
+the API (e.g., one request per second, tops, allowed).
+@end defun
+
+@defun olc-is-valid code
+Returns non-@code{nil} if @var{code} is a valid open location code.
+@end defun
+
+@defun olc-is-short code
+Returns non-@code{nil} if @var{code} is a valid short location code.
+Returns @code{nil} for valid short and for invalid codes.
+@end defun
+
+@defun olc-is-full code
+Returns non-@code{nil} if @var{code} is a valid full open location
+code. Returns @code{nil} for valid long and for invalid codes.
+@end defun
+
+
+@node Index,,Functions,Top
+@unnumbered Index
+
+@printindex tp
+@printindex fn
+
+@bye
diff --git a/test/olctest.el b/test/olctest.el
index bdc9f18b96df58756d8adbee037b5f642eaf68be..bf2af42554817625fa02214ec1085f7fc5f1c254 100644
--- a/test/olctest.el
+++ b/test/olctest.el
@@ -64,15 +64,26 @@
       (kill-buffer buffer))))
 
 
-(defmacro olctest-run-tests (spec &rest body)
+(defmacro olctest-run-csv (spec &rest body)
   "Run open location code tests.
 
 \(fn (VAR LIST) BODY...)"
   (declare (indent 1) (debug ((form symbolp) body)))
   (let ((data (gensym "$olctest")))
-    `(let ((,data (olctest-read-csv ,(elt spec 0)))
-           ($olctest-results nil))
-       (setq foo ,data)
+    `(let* ((,data (olctest-read-csv ,(elt spec 0)))
+            ($olctest-results nil))
+       (dolist (,(elt spec 1) ,data)
+         ,@body)
+       (olctest-report-results $olctest-results))))
+
+(defmacro olctest-run-list (spec &rest body)
+  "Run open location code tests.
+
+\(fn (VAR LIST) BODY...)"
+  (declare (indent 1) (debug ((form symbolp) body)))
+  (let ((data (gensym "$olctest")))
+    `(let* ((,data ,(elt spec 0))
+            ($olctest-results nil))
        (dolist (,(elt spec 1) ,data)
          ,@body)
        (olctest-report-results $olctest-results))))
@@ -97,7 +108,7 @@
 
 (defun olctest-encode ()
   "Test encoding."
-  (olctest-run-tests ("encoding.csv" case)
+  (olctest-run-csv ("encoding.csv" case)
     (let ((code (olc-encode (alist-get 'latitude case)
                             (alist-get 'longitude case)
                             (alist-get 'length case))))
@@ -107,14 +118,14 @@
 
 (defun olctest-decode ()
   "Test decoding."
-  (olctest-run-tests ("decoding.csv" case)
+  (olctest-run-csv ("decoding.csv" case)
     (let ((area (olc-decode (alist-get 'code case)))
           (exp-latlo (alist-get 'latLo case))
           (exp-lathi (alist-get 'latHi case))
           (exp-lonlo (alist-get 'lngLo case))
           (exp-lonhi (alist-get 'lngHi case))
           (exp-len (alist-get 'length case)))
-      (unless (and (= exp-len (olc-code-length (alist-get 'code case)))
+      (unless (and (= exp-len (olc-code-precision (alist-get 'code case)))
                    (< (abs (- (olc-area-latlo area) exp-latlo)) olctest-decode-tolerance)
                    (< (abs (- (olc-area-lathi area) exp-lathi)) olctest-decode-tolerance)
                    (< (abs (- (olc-area-lonlo area) exp-lonlo)) olctest-decode-tolerance)
@@ -122,7 +133,7 @@
         (olctest-record-failure case
                                 (format "%d,%f,%f,%f,%f" exp-len exp-latlo exp-lonlo exp-lathi exp-lonhi)
                                 (format "%d,%f,%f,%f,%f"
-                                        (olc-code-length (alist-get 'code case))
+                                        (olc-code-precision (alist-get 'code case))
                                         (olc-area-latlo area)
                                         (olc-area-lonlo area)
                                         (olc-area-lathi area)
@@ -131,7 +142,7 @@
 
 (defun olctest-shortcodes ()
   "Test recovering."
-  (olctest-run-tests ("shortCodeTests.csv" case)
+  (olctest-run-csv ("shortCodeTests.csv" case)
     (let ((fullcode (alist-get 'fullcode case))
           (lat (alist-get 'lat case))
           (lon (alist-get 'lng case))
@@ -146,13 +157,15 @@
 
       ;; Test shorten
       (when (or (string= test-type "B") (string= test-type "S"))
-        ;; Shorten is not implemented
-        )
+        (let ((shortened (olc-shorten fullcode lat lon)))
+          (unless (string= shortened shortcode)
+            (olctest-record-failure case shortcode shortened))))
       )))
 
+
 (defun olctest-validity ()
   "Test validity."
-  (olctest-run-tests ("validityTests.csv" case)
+  (olctest-run-csv ("validityTests.csv" case)
     (let* ((code (alist-get 'code case))
            (expected (list (alist-get 'isValid case)
                            (alist-get 'isShort case)
@@ -163,12 +176,29 @@
       (unless (equal expected actual)
         (olctest-record-failure case expected actual)))))
 
+(defvar olctest-local-shorten-tests
+  '(((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 8) (exp . "+2VX"))
+    ((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 6) (exp . "CJ+2VX"))
+    ((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 4) (exp . "9QCJ+2VX"))
+    ((code . "9C3W9QCJ+2VX") (lat . 51.3701125) (lon . -1.217765625) (len . 2) (exp . "3W9QCJ+2VX"))))
+
+(defun olctest-localtests ()
+  (olctest-run-list (olctest-local-shorten-tests case)
+    (let* ((fullcode (alist-get 'code case))
+           (lat (alist-get 'lat case))
+           (lon (alist-get 'lon case))
+           (len (alist-get 'len case))
+           (shortcode (alist-get 'exp case))
+           (actual (olc-shorten fullcode lat lon len)))
+      (unless (string= actual shortcode)
+        (olctest-record-failure case shortcode actual)))))
+
 
 (defun olctest-run-all ()
   "Run all tests."
   (and (olctest-decode)
        (olctest-encode)
        (olctest-shortcodes)
-       (olctest-validity)))
-
-
+       (olctest-validity)
+       (olctest-localtests)
+       ))