All the Text functions assume the cursor is always on a text line. I was violating that invariant.
When scrolling up, I start the cursor at the top-most line below the screen top.
When scrolling down, I start the cursor at the top-most line below the screen bottom.
I think it would feel slightly more natural for it to be the bottom-most line above the screen bottom.
However, the Text functions maintain an invariant that the bottom-most line in a buffer will be text. There's no such invariant for the top-most line.
2MGBV7NPY2RVK7EZ7OJRWBYDUMADFZAFSBKLQFX74ERBVZODJLUQC